diff --git a/diagnostic/index.html b/diagnostic/index.html index bbdbde7..cb95772 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -5156,13 +5156,28 @@ const meta = WORKER_SVC_META[w.service] || { icon: '⚙️', label: w.service }; const dot = !w.online ? '#666' : (w.busy ? '#FFB020' : '#3FFF3F'); const stat = !w.online ? 'offline' : (w.busy ? 'beschaeftigt' : 'frei – idle – keine Auslastung'); - // llm-Boxen koennen mehrere Modelle fahren (llama-swap) → Liste zeigen. - const modelText = (w.service === 'llm' && Array.isArray(w.models) && w.models.length) - ? w.models.join(', ') : (w.model || ''); + // llm-Boxen koennen mehrere Modelle FAHREN (llama-swap), aber nur + // eins liegt gerade in der Karte — das ist gruen markiert. Die + // anderen werden beim ersten Aufruf geladen (und verdraengen es). + let modelHtml; + if (w.service === 'llm' && Array.isArray(w.models) && w.models.length) { + const loaded = Array.isArray(w.loaded) ? w.loaded : null; + const isLoaded = id => loaded && loaded.some(l => l.model === id); + const state = id => (loaded.find(l => l.model === id) || {}).state; + modelHtml = w.models.map(id => isLoaded(id) + ? '' + escapeHtml(id) + + (state(id) && state(id) !== 'ready' ? ' (' + escapeHtml(state(id)) + ' …)' : ' ● geladen') + '' + : '' + escapeHtml(id) + '' + ).join(', '); + if (loaded && !loaded.length) modelHtml += ' — keins geladen (lädt beim ersten Aufruf)'; + else if (!loaded && w.online) modelHtml += ' — geladen: ?'; + } else { + modelHtml = '' + escapeHtml(w.model || '') + ''; + } return '
' + '' + '' + meta.icon + ' ' + escapeHtml(meta.label) + '' + - '' + escapeHtml(modelText) + '' + + '' + modelHtml + '' + (w.gpus ? 'GPU ' + escapeHtml(w.gpus) + '' : '') + '' + stat + '' + '
' + diff --git a/diagnostic/server.js b/diagnostic/server.js index 118e1c7..6d8225c 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -1260,6 +1260,9 @@ function connectRVS(forcePlain) { instanceId: p.instanceId, service: p.service || "", node: p.node || "", gpus: p.gpus || "", model: p.model || "", models: Array.isArray(p.models) ? p.models : (p.model ? [p.model] : []), + // Was GERADE in der Karte liegt (nur llm, aus llama-swap /running). + // null = Box konnte llama-swap nicht fragen, [] = nichts geladen. + loaded: Array.isArray(p.loaded) ? p.loaded : (prev.loaded ?? null), // OpenAI-Endpunkt (nur llm-Worker): Port/Bind kommen aus der .env der // Box, der Key wird NIE uebertragen — nur ob einer gesetzt ist und ob // die .env ihn vorgibt (dann ist das Feld hier schreibgeschuetzt). @@ -1282,6 +1285,7 @@ function connectRVS(forcePlain) { workers.set(p.instanceId, w); } w.busy = !!p.busy; + if ("loaded" in p) w.loaded = Array.isArray(p.loaded) ? p.loaded : null; w.last_seen = Date.now(); broadcastWorkers(); } diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index cf7a3ea..677ff00 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -315,14 +315,43 @@ async def _fetch_available_models() -> list: return [LLM_MODEL] +async def _fetch_loaded() -> list: + """Welche Modelle liegen GERADE in der Karte? llama-swap /running → + [{"model", "state"}]. Ohne groups-Config ist das hoechstens eins — ein + anderes anzufragen entlaedt es. None, wenn llama-swap nicht antwortet + ("unbekannt" ist etwas anderes als "nichts geladen").""" + try: + async with httpx.AsyncClient(timeout=5) as client: + r = await client.get(f"{LLAMA_URL}/running") + r.raise_for_status() + return [{"model": m.get("model", ""), "state": m.get("state", "")} + for m in (r.json().get("running") or []) if m.get("model")] + except Exception as e: + logger.debug("llama-swap /running nicht abfragbar: %s", e) + return None + + +async def _ping(ws) -> None: + """Lebenszeichen mit busy + geladenem Modell. loaded=None heisst "konnte + llama-swap nicht fragen" — das ist etwas anderes als "nichts geladen".""" + await _send(ws, "worker_ping", + {"instanceId": INSTANCE_ID, "busy": _inflight > 0, + "loaded": await _fetch_loaded()}) + + async def _announce(ws) -> None: """Sendet ein frisches worker_hello mit der aktuellen Modell-Liste (nach Provision/Remove aufrufen, damit Bridge+Diagnostic das neue Modell lernen).""" models = await _fetch_available_models() await _send(ws, "worker_hello", { "instanceId": INSTANCE_ID, "service": WORKER_SERVICE, - "node": NODE_NAME, "gpus": GPU_IDS, "model": LLM_MODEL, + # Die LLM-Karte ist die von llama-swap (LLM_GPU). Die eigene + # NVIDIA_VISIBLE_DEVICES des Adapters ist "all" bzw. wird vom + # Container-Toolkit ersetzt (Anzeige "GPU void") — sagt nichts darueber, + # wo das Modell laeuft. + "node": NODE_NAME, "gpus": LLM_GPU or GPU_IDS, "model": LLM_MODEL, "models": models, # welche Modelle diese Box fahren kann (llama-swap-Keys) + "loaded": await _fetch_loaded(), # welches davon GERADE in der Karte liegt # OpenAI-Endpunkt fuer Fremd-Tools (OpenHands & Co.). Der Key selbst wird # NICHT gesendet (Broadcast!) — nur ob einer gesetzt ist und ob die .env # ihn vorgibt (dann ist das Feld in der Diagnostic schreibgeschuetzt). @@ -343,8 +372,7 @@ async def _worker_register(ws) -> None: while True: await asyncio.sleep(WORKER_PING_INTERVAL_S) n += 1 - await _send(ws, "worker_ping", - {"instanceId": INSTANCE_ID, "busy": _inflight > 0}) + await _ping(ws) if n % 3 == 0: # ~30s worker_hello wiederholen (wie der Satellit) → await _announce(ws) # auch neu gestartetes Diagnostic/Bridge lernt uns except asyncio.CancelledError: @@ -396,6 +424,7 @@ async def _do_llm_request(ws, payload: dict) -> None: _last_model = eff_model await _emit_llm_status(ws, "ready", eff_model, loadSeconds=round(dt, 1), freshlyDownloaded=fresh) + await _ping(ws) # Diagnostic zeigt den Wechsel sofort, nicht erst in 10 s else: # bei Fehler _last_model NICHT setzen → naechster Versuch meldet erneut loading await _emit_llm_status(ws, "error", eff_model, @@ -637,6 +666,7 @@ async def _handle_provision(ws, payload: dict) -> None: freshlyDownloaded=dt > 25) await _emit_progress(ws, key, "ready", percent=100, loadSeconds=round(dt, 1), **(await _gpu_info())) + await _ping(ws) else: await _emit_llm_status(ws, "error", key, error=(res.get("error") or "")[:160]) await _emit_progress(ws, key, "error", error=(res.get("error") or "")[:200])