mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-01 02:24:34 -04:00
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 <mudler@localai.io>
This commit is contained in:
1 parent
102fbe0c78
commit
590512d9eb
7 files changed
+66
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
<h3>Node</h3>
|
||||
<dl className="node-inspector__metrics">
|
||||
<InspectorMetric label="Address"><span className="node-inspector__address">{node.address || 'No address reported'}</span></InspectorMetric>
|
||||
<InspectorMetric label="Version">{node.version || '—'}</InspectorMetric>
|
||||
<InspectorMetric label="Heartbeat">{timeAgo(node.last_heartbeat)}</InspectorMetric>
|
||||
</dl>
|
||||
<div className="node-inspector__labels" aria-label="Node labels">{Object.keys(node.labels || {}).length ? Object.entries(node.labels).map(([key, value]) => <span key={key}>{key}={value}</span>) : <span className="text-muted">No labels</span>}</div>
|
||||
@@ -94,6 +110,26 @@ export default function NodeInspector({ node, open, onClose, onApprove, onDrain,
|
||||
<InspectorMetric label="Backends">{backendError ? <span className="text-error">{backendError}</span> : backends === null ? 'Loading…' : `${backends.length} backend${backends.length === 1 ? '' : 's'}`}</InspectorMetric>
|
||||
<InspectorMetric label="In-flight work">{node.in_flight_count ?? 0}</InspectorMetric>
|
||||
</dl>
|
||||
<div className="node-inspector__models">
|
||||
<dt className="drawer-eyebrow">Running models</dt>
|
||||
<dd>
|
||||
{modelError ? <span className="text-error">{modelError}</span>
|
||||
: models === null ? <span className="text-muted">Loading…</span>
|
||||
: models.length === 0 ? <span className="text-muted">No models loaded</span>
|
||||
: <ul className="node-inspector__model-list">
|
||||
{models.map(model => {
|
||||
const stCfg = modelStateConfig[model.state] || modelStateConfig.idle
|
||||
return (
|
||||
<li key={model.id || `${model.model_name}#${model.replica_index}`} className="node-inspector__model-row">
|
||||
<span className="cell-mono">{model.model_name}</span>
|
||||
<span className="state-pill" style={{ background: stCfg.bg, color: stCfg.color, border: `1px solid ${stCfg.border}` }}>{model.state}</span>
|
||||
<span className="text-muted">{model.in_flight ?? 0} in flight</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>}
|
||||
</dd>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer className="node-inspector__actions">
|
||||
|
||||
@@ -138,6 +138,10 @@ export default function NodeDetail() {
|
||||
<div className="drawer-eyebrow">In-flight</div>
|
||||
<span className="cell-mono">{node.in_flight_count || 0}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="drawer-eyebrow">Version</div>
|
||||
<span className="cell-mono">{node.version || '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="drawer-eyebrow">Heartbeat</div>
|
||||
<span>{timeAgo(node.last_heartbeat)}</span>
|
||||
|
||||
@@ -89,6 +89,11 @@ type BackendNode struct {
|
||||
// worker's re-registration value does not clobber it (mirrors
|
||||
// MaxReplicasPerModelManuallySet).
|
||||
VRAMBudgetManuallySet bool `gorm:"column:vram_budget_manually_set;default:false" json:"vram_budget_manually_set"`
|
||||
// Version is the LocalAI build version reported by the worker at
|
||||
// registration. Empty for workers registered before this field existed.
|
||||
Version string `gorm:"column:version;size:64" json:"version,omitempty"`
|
||||
// Commit is the git commit hash the worker binary was built from.
|
||||
Commit string `gorm:"column:commit;size:64" json:"commit,omitempty"`
|
||||
APIKeyID string `gorm:"size:36" json:"-"` // auto-provisioned API key ID (for cleanup)
|
||||
AuthUserID string `gorm:"size:36" json:"-"` // auto-provisioned user ID (for cleanup)
|
||||
LastHeartbeat time.Time `gorm:"column:last_heartbeat" json:"last_heartbeat"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/internal"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/mudler/LocalAI/pkg/xsysinfo"
|
||||
"github.com/mudler/xlog"
|
||||
@@ -173,6 +174,8 @@ func (cfg *Config) registrationBody() map[string]any {
|
||||
"gpu_compute_capability": gpuComputeCap,
|
||||
"capability": capability,
|
||||
"max_replicas_per_model": maxReplicas,
|
||||
"version": internal.Version,
|
||||
"commit": internal.Commit,
|
||||
}
|
||||
|
||||
// Report free space on the filesystem that backs the MODELS directory.
|
||||
|
||||
Reference in new issue
Block a user