Feat: Hirn-Bereitschaft im /api/health + Frontend-Warnung

/api/health liefert jetzt brain:{role,model,ready} (echter /running-Check —
ein abgestuerztes/nicht geladenes Agent-Hirn 'fast' erscheint dort nicht).
Frontend zeigt 'Hirn offline (model)' + Amber-Punkt, statt dass der Ausfall
nur als App-Fehler ('Provider returned an empty stream') auftaucht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 21:42:42 +02:00
parent 181db38b49
commit 63a83c69c3
7 changed files with 44 additions and 6 deletions
+3
View File
@@ -15,4 +15,7 @@ def health() -> dict:
"version": VERSION, "version": VERSION,
"engine_reachable": llamaswap.engine_reachable(), "engine_reachable": llamaswap.engine_reachable(),
"gateway_reachable": gateway.gateway_reachable(), "gateway_reachable": gateway.gateway_reachable(),
# Echte Hirn-Bereitschaft: Engine kann erreichbar sein, das Agent-Hirn ('fast') aber tot
# (Crash/OOM nach Engine-Update). Das wäre sonst ein silent fail (App-Fehler statt Status).
"brain": llamaswap.brain_status(),
} }
+20
View File
@@ -432,6 +432,26 @@ def delete_model(model_id: str) -> bool:
return True return True
def brain_model_name() -> str | None:
"""Modellname des Agent-Hirns = das Modell mit llama-swap-Alias/Rolle 'fast'."""
for m in list_models():
names = {str(a).lower() for a in (m.get("aliases") or [])}
if m.get("role"):
names.add(str(m["role"]).lower())
if "fast" in names:
return m["name"]
return None
def brain_status() -> dict:
"""Ist das Agent-Hirn ('fast') WIRKLICH geladen & bereit? Prüft /running — ein abgestürztes
Modell (z.B. OOM/Crash nach Engine-Update) erscheint dort NICHT als running. Fängt damit den
Fall 'Engine erreichbar, aber Hirn tot', den engine_reachable() nicht sieht (silent fail)."""
name = brain_model_name()
running = get_running_models()
return {"role": "fast", "model": name, "ready": bool(name and name in running)}
def get_running_models() -> list[str]: def get_running_models() -> list[str]:
"""Fragt den /running Endpunkt von llama-swap ab. Gibt die Namen der geladenen """Fragt den /running Endpunkt von llama-swap ab. Gibt die Namen der geladenen
Modelle zurück. Neuere llama-swap-Versionen liefern Objekte ({model, state, ...}) Modelle zurück. Neuere llama-swap-Versionen liefern Objekte ({model, state, ...})
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-4xoYNTUx.js"></script> <script type="module" crossorigin src="/assets/index-BcdAxUp3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BFWF7uFR.css"> <link rel="stylesheet" crossorigin href="/assets/index-BFWF7uFR.css">
</head> </head>
<body> <body>
+16 -2
View File
@@ -119,16 +119,30 @@ export default function App() {
<div className="flex justify-center"> <div className="flex justify-center">
<span className={cn( <span className={cn(
"h-2.5 w-2.5 rounded-full ring-2 ring-black/40", "h-2.5 w-2.5 rounded-full ring-2 ring-black/40",
health ? (health.engine_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500") : "bg-red-500" health
)} title={health ? `Engine ${health.engine_reachable ? "online" : "offline"}` : "Backend offline"} /> ? (!health.engine_reachable ? "bg-amber-500"
: (health.brain && !health.brain.ready ? "bg-amber-500 animate-pulse" : "bg-emerald-500 animate-pulse"))
: "bg-red-500"
)} title={health
? (!health.engine_reachable ? "Engine offline"
: (health.brain && !health.brain.ready ? `Hirn offline (${health.brain.model ?? "fast"})` : "Engine + Hirn online"))
: "Backend offline"} />
</div> </div>
) : ( ) : (
<div className="space-y-2 text-left"> <div className="space-y-2 text-left">
{health ? ( {health ? (
<>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className={cn("h-2 w-2 rounded-full animate-pulse", health.engine_reachable ? "bg-emerald-500" : "bg-amber-500")} /> <span className={cn("h-2 w-2 rounded-full animate-pulse", health.engine_reachable ? "bg-emerald-500" : "bg-amber-500")} />
<span className="truncate">Engine {health.engine_reachable ? "online" : "offline"}</span> <span className="truncate">Engine {health.engine_reachable ? "online" : "offline"}</span>
</span> </span>
{health.brain && !health.brain.ready && (
<span className="flex items-center gap-2 text-amber-400" title={`Agent-Hirn '${health.brain.model ?? "fast"}' lädt nicht/abgestürzt`}>
<span className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
<span className="truncate">Hirn offline{health.brain.model ? ` (${health.brain.model})` : ""}</span>
</span>
)}
</>
) : ( ) : (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-red-500" /> <span className="truncate">Backend offline</span> <span className="h-2 w-2 rounded-full bg-red-500" /> <span className="truncate">Backend offline</span>
+1
View File
@@ -351,6 +351,7 @@ export interface Health {
version: string version: string
engine_reachable: boolean engine_reachable: boolean
gateway_reachable: boolean gateway_reachable: boolean
brain?: { role: string; model: string | null; ready: boolean }
} }
export interface TokenStats { export interface TokenStats {