chore: merge master into distributed transport PR

Keep the version-reporting import from master and omit the unused
sanitize import after the transport changes.

Assisted-by: Codex:GPT-6
This commit is contained in:
localai-org-maint-bot committed 2026-09-28 12:02:15 +00:00
commit 4151ceda85
51 files changed
+1785 -72

No files matched your search

+7
View File
@@ -114,6 +114,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.
@@ -191,6 +196,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()
+7
View File
@@ -9966,6 +9966,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>