mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-30 18:14:32 -04:00
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:
commit
4151ceda85
51 files changed
+1785
-72
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user