feat(llm): Flotten-Katalog + modell-bewusstes LLM-Routing (Stage C)

ARIA nutzt lokale LLMs auf mehreren ai-boxen; jede Box (llama-swap) kann
mehrere Modelle fahren. Auswahl nach MODELL, Box wird automatisch gewaehlt.

- xtts/llm-adapter/adapter.py: fragt beim Connect llama-swap GET /v1/models ab
  und meldet die Modell-Liste in worker_hello (models:[...]), Fallback [LLM_MODEL].
- bridge/aria_bridge.py: Worker-Registry speichert models[]; _pick_worker(service,
  model=) beruecksichtigt nur Boxen, die das Modell fahren koennen (nachsichtig:
  keine → None → Broadcast/Claude-Fallback); Round-Robin je service+model
  verteilt mehrere Projekte auf mehrere Boxen; _local_llm reicht das Modell durch;
  _worker_list traegt models[].
- diagnostic/server.js: workers-Map + workerList um models[] erweitert.
- diagnostic/index.html: Compute-Flotte zeigt bei llm die Modell-Liste; das
  "Lokales Modell"-Dropdown wird LIVE aus den angemeldeten Boxen gebaut
  (Vereinigung + kuratierte Namen aus local_models.json, "· N Box(en)"/"offline"),
  darunter eine kompakte LLM-Box-Liste (Node→Modelle→Health). Speisung aus dem
  vorhandenen worker_update-Broadcast.

Kein Brain-/App-Eingriff. Routing greift nur bei aktivem Lokal-Schalter.
Deploy: diagnostic + bridge + llm-Boxen neu bauen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 02:49:30 +02:00
co-authored by Claude Opus 4.8
parent 0f122a1ad7
commit 05c6c7687a
4 changed files with 114 additions and 26 deletions
+26 -10
View File
@@ -3544,12 +3544,17 @@ class ARIABridge:
iid = (payload.get("instanceId") or "").strip()
if iid:
prev = self._workers.get(iid, {})
_models = payload.get("models")
self._workers[iid] = {
"instanceId": iid,
"service": payload.get("service") or "",
"node": payload.get("node") or "",
"gpus": payload.get("gpus") or "",
"model": payload.get("model") or "",
# models: welche Modelle die Box fahren kann (llm/llama-swap).
# Fallback auf [model] fuer alte Adapter ohne models-Feld.
"models": [m for m in _models if m] if isinstance(_models, list)
else ([payload.get("model")] if payload.get("model") else []),
"busy": bool(prev.get("busy", False)),
"last_seen": time.time(),
}
@@ -4006,9 +4011,10 @@ class ARIABridge:
req_payload["tools"] = tools
if model:
req_payload["model"] = model
# Redundanz/Multitasking: freie llm-Instanz gezielt adressieren; None
# → Broadcast wie bisher. Mehrere Instanzen → parallele Turns.
llm_target = self._pick_worker("llm")
# Redundanz/Multitasking: freie llm-Instanz gezielt adressieren, die
# das gewaehlte Modell fahren kann; None → Broadcast wie bisher.
# Mehrere Boxen mit demselben Modell → Round-Robin (pro Projekt verteilt).
llm_target = self._pick_worker("llm", model=model or None)
if llm_target:
req_payload["targetInstance"] = llm_target
logger.info("[rvs] llm_request → llm-adapter (id=%s, msgs=%d, max_tokens=%d, tools=%d, model=%s, target=%s)",
@@ -4767,29 +4773,39 @@ class ARIABridge:
out.append({
"instanceId": w["instanceId"], "service": w.get("service") or "",
"node": w.get("node") or "", "gpus": w.get("gpus") or "",
"model": w.get("model") or "", "busy": bool(w.get("busy")),
"model": w.get("model") or "", "models": w.get("models") or [],
"busy": bool(w.get("busy")),
"online": (now - w.get("last_seen", 0)) < self.WORKER_OFFLINE_S,
})
return out
def _pick_worker(self, service: str) -> Optional[str]:
def _pick_worker(self, service: str, model: Optional[str] = None) -> Optional[str]:
"""Waehlt eine online, moeglichst freie Instanz des Diensts (Round-Robin
ueber die freien). Gibt die instanceId oder None (keine online). Fuer
Stage-3-Routing (targetInstance)."""
ueber die freien). Gibt die instanceId oder None. Fuer Stage-3-Routing
(targetInstance).
model: wenn gesetzt (nur llm sinnvoll), kommen nur Boxen in Frage, die das
Modell fahren koennen (models-Liste oder legacy model-Feld). Meldet KEINE
Box das Modell → None (nachsichtig: Aufrufer faellt auf Broadcast zurueck)."""
now = time.time()
online = [w for w in self._workers.values()
if w.get("service") == service
and (now - w.get("last_seen", 0)) < self.WORKER_OFFLINE_S]
if model:
online = [w for w in online
if model in (w.get("models") or [])
or w.get("model") == model]
if not online:
return None
free = [w for w in online if not w.get("busy")]
pool = free or online # alle busy → trotzdem eine nehmen (least-bad)
# Round-Robin: rotierender Zeiger pro Dienst.
# Round-Robin: rotierender Zeiger pro Dienst(+Modell).
rr_key = f"{service}:{model}" if model else service
rr = getattr(self, "_worker_rr", None)
if rr is None:
rr = self._worker_rr = {}
idx = rr.get(service, 0) % len(pool)
rr[service] = idx + 1
idx = rr.get(rr_key, 0) % len(pool)
rr[rr_key] = idx + 1
chosen = pool[idx]
chosen["busy"] = True # optimistisch, bis der naechste Ping korrigiert
return chosen["instanceId"]