From d4d3e13a230bae34970f028be9397bb3d80dead1 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 11:25:27 -0400 Subject: [PATCH 1/2] feat(dashboard): Model Control flag-card UI (consumes /model-config) LaunchDarkly/Firebase-style control plane for llama.cpp launch params: a "Model" tab where every flag (model, ctx, rope/YaRN, override-kv, KV quant, MTP, mmproj, gen-caps, ...) is a typed, validated field with an inherited/override pill + reset, a model dropdown, and one "Apply & restart" that POSTs the diff. - routes_model_config.py: GET/POST /api/model-config proxy to ops-controller (token injected server-side, like routes_registry). - index.html: Model tab + loadModelControl()/renderModelControl()/apply (grouped flag cards from GET /model-config; Apply -> POST overrides -> recreate). Depends on the control-plane API in #55. JS validated via node --check. Co-Authored-By: Claude Opus 4.8 (1M context) --- dashboard/app.py | 2 + dashboard/routes_model_config.py | 38 +++++++++++ dashboard/static/index.html | 104 ++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 dashboard/routes_model_config.py diff --git a/dashboard/app.py b/dashboard/app.py index aff0447..2340853 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -31,6 +31,7 @@ from dashboard import gpu_stats, settings from dashboard import routes_gpu as _routes_gpu +from dashboard import routes_model_config as _routes_model_config from dashboard import routes_registry as _routes_registry from dashboard.orchestration_db import get_job_counts, get_outbox_stats from dashboard.routes_hub import router as hub_router @@ -2118,6 +2119,7 @@ def _empty_payload(): _routes_gpu.register(app, _ops_request) _routes_registry.register(app, _ops_request) +_routes_model_config.register(app, _ops_request) mimetypes.add_type("application/octet-stream", ".stl") diff --git a/dashboard/routes_model_config.py b/dashboard/routes_model_config.py new file mode 100644 index 0000000..88a78f7 --- /dev/null +++ b/dashboard/routes_model_config.py @@ -0,0 +1,38 @@ +"""Model-config control-plane passthrough to ops-controller (/model-config). + +Thin proxy (like routes_registry): the browser never sees the ops-controller +token β€” _ops_request injects it. Drives the dashboard's Model Control flag UI. +""" +from __future__ import annotations + +from fastapi import APIRouter, HTTPException, Request + +router = APIRouter(prefix="/api/model-config") + +_ops_request = None +_registered = False + + +def register(app, ops_request): + """Wire routes onto `app`. `ops_request` is dashboard.app._ops_request.""" + global _ops_request, _registered + _ops_request = ops_request + if _registered: + return + _registered = True + + @router.get("") + async def get_model_config(request: Request): + code, data = await _ops_request("GET", "/model-config", request=request) + if code >= 400: + raise HTTPException(status_code=code, detail=(data.get("detail", data) if isinstance(data, dict) else data)) + return data + + @router.post("") + async def post_model_config(body: dict, request: Request): + code, data = await _ops_request("POST", "/model-config", request=request, json=body) + if code >= 400: + raise HTTPException(status_code=code, detail=(data.get("detail", data) if isinstance(data, dict) else data)) + return data + + app.include_router(router) diff --git a/dashboard/static/index.html b/dashboard/static/index.html index 873cddc..6be9a88 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -1646,6 +1646,7 @@

Ordo AI Stack Dashboard

+ @@ -1988,6 +1989,14 @@

Model Registry

+
+
+

Model Control

+

All llama.cpp launch parameters as feature flags. Baseline = .env defaults; per-model overrides live in the registry. "Apply & restart" renders the effective config to .env and recreates llamacpp (+ model-gateway when context changes).

+

Loading…

+
+
+

Orchestration

@@ -2690,6 +2699,98 @@

Dashboard login

loadGpuTab(); } + // --- Model Control: flag-card UI over /api/model-config (control plane) --- + let _mcState = null, _mcDirty = {}; + const MC_GROUPS = [["core","Core"],["context","Context extension"],["attention","Attention / KV"],["mtp","Speculative (MTP)"],["gen","Generation caps"],["multimodal","Multimodal"],["advanced","Advanced"]]; + + async function loadModelControl() { + const root = document.getElementById('modelctl-root'); + if (!root) return; + root.innerHTML = '

Loading…

'; + try { + const r = await api('/api/model-config'); + if (!r.ok) { root.innerHTML = '

Failed to load model config (' + r.status + ')

'; return; } + _mcState = await r.json(); _mcDirty = {}; + renderModelControl(); + } catch (e) { root.innerHTML = '

' + e + '

'; } + } + + function _mcMeta(key) { return (_mcState.flags || []).find(f => f.key === key); } + + function _mcInput(f, val) { + const k = f.key; + if (f.kind === 'enum' && f.choices) return ''; + if (f.kind === 'bool') return ''; + const t = (f.kind === 'int' || f.kind === 'float') ? 'number' : 'text'; + return ''; + } + + function renderModelControl() { + const s = _mcState, eff = s.effective || {}, ov = s.overrides || {}; + const byGroup = {}; + (s.flags || []).forEach(f => { (byGroup[f.group] = byGroup[f.group] || []).push(f); }); + let h = '
'; + h += 'Active model
'; + MC_GROUPS.forEach(([g, label]) => { + const fs = byGroup[g]; if (!fs) return; + h += '
' + label + '
'; + fs.forEach(f => { + if (f.key === 'LLAMACPP_MODEL') return; + const overridden = (f.key in ov) || (g === 'mtp' && eff['MTP_ENABLED'] === '1'); + const val = eff[f.key] !== undefined ? eff[f.key] : (f.default || ''); + h += '
'; + h += '' + f.key.replace('LLAMACPP_', '').toLowerCase() + ''; + h += _mcInput(f, val); + h += '' + (overridden ? 'override' : 'inherited') + ''; + h += ''; + h += '
'; + }); + h += '
'; + }); + document.getElementById('modelctl-root').innerHTML = h; + _wireModelControl(); + } + + function _wireModelControl() { + document.querySelectorAll('#modelctl-root .mc-in[data-key]').forEach(el => { + const upd = () => { _mcDirty[el.dataset.key] = el.value; _mcUpdateDirty(); }; + el.addEventListener('input', upd); el.addEventListener('change', upd); + }); + document.querySelectorAll('#modelctl-root .mc-reset').forEach(b => { + b.addEventListener('click', () => { + _mcDirty[b.dataset.key] = null; _mcUpdateDirty(); + const f = _mcMeta(b.dataset.key); + const inp = document.querySelector('#modelctl-root .mc-flag[data-key="' + b.dataset.key + '"] .mc-in'); + if (inp && f) inp.value = f.default || ''; + }); + }); + const ms = document.getElementById('mc-model'); + if (ms) ms.addEventListener('change', () => { _mcDirty['LLAMACPP_MODEL'] = ms.value; _mcUpdateDirty(); }); + document.getElementById('mc-apply')?.addEventListener('click', _applyModelControl); + _mcUpdateDirty(); + } + + function _mcUpdateDirty() { + const n = Object.keys(_mcDirty).length; + const d = document.getElementById('mc-dirty'); + if (d) d.textContent = n ? (n + ' change' + (n > 1 ? 's' : '') + ' pending') : 'no changes'; + } + + async function _applyModelControl() { + if (!Object.keys(_mcDirty).length) { toast('No changes to apply'); return; } + const btn = document.getElementById('mc-apply'); + btn.disabled = true; btn.textContent = 'Applying…'; + try { + const r = await api('/api/model-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true, overrides: _mcDirty }) }); + const data = await r.json().catch(() => ({})); + if (!r.ok) { toast('Apply failed: ' + JSON.stringify(data.detail || data), 'error'); btn.disabled = false; btn.textContent = 'Apply & restart'; return; } + toast('Applied β€” recreating ' + ((data.recreated || []).join(', ') || 'llamacpp'), 'success'); + await loadModelControl(); + } catch (e) { toast('Apply error: ' + e, 'error'); btn.disabled = false; btn.textContent = 'Apply & restart'; } + } + async function loadOrchestrationTab() { const rd = document.getElementById("orch-readiness"); const jobsEl = document.getElementById("orch-jobs"); @@ -3967,7 +4068,7 @@

Dashboard login

// ── End Compute Pressure ────────────────────────────────── function activateTab(name) { - const tabs = ["models", "gpu", "registry", "services", "mcp", "orchestration"]; + const tabs = ["models", "gpu", "registry", "modelctl", "services", "mcp", "orchestration"]; if (!tabs.includes(name)) name = "models"; document.querySelectorAll(".tab-btn").forEach(b => { const on = b.dataset.tab === name; @@ -3986,6 +4087,7 @@

Dashboard login

if (typeof loadModelsRegistry === "function") loadModelsRegistry(); } if (name === "orchestration" && typeof loadOrchestrationTab === "function") loadOrchestrationTab(); + if (name === "modelctl" && typeof loadModelControl === "function") loadModelControl(); } document.querySelectorAll(".tab-btn").forEach(b => b.addEventListener("click", () => activateTab(b.dataset.tab))); window.addEventListener("hashchange", () => activateTab(location.hash.replace("#", ""))); From e8dd6f7b47c90045131ae2d77b3981e6a1030fe3 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 11:54:58 -0400 Subject: [PATCH 2/2] feat(dashboard): flag tooltips (title + info icon) from descriptor help Each flag label shows a native tooltip + a hover (i) marker sourced from the schema's help text (GET /model-config descriptors). Co-Authored-By: Claude Opus 4.8 (1M context) --- dashboard/static/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboard/static/index.html b/dashboard/static/index.html index 6be9a88..000bc41 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -2741,7 +2741,8 @@

Dashboard login

const overridden = (f.key in ov) || (g === 'mtp' && eff['MTP_ENABLED'] === '1'); const val = eff[f.key] !== undefined ? eff[f.key] : (f.default || ''); h += '
'; - h += '' + f.key.replace('LLAMACPP_', '').toLowerCase() + ''; + const _help = (f.help || '').replace(/"/g, '"'); + h += '' + f.key.replace('LLAMACPP_', '').toLowerCase() + (f.help ? ' β“˜' : '') + ''; h += _mcInput(f, val); h += '' + (overridden ? 'override' : 'inherited') + ''; h += '';