#!/usr/bin/env bash # Prüfstand-Runner (Täglich 02:00, ehemals Wöchentlich): Baut die Umgebung für den # Harness (Modell-Sandkasten, GPU-RAM-Check) und zieht die Fäden. # Läuft als Cron (Täglich 02:00, nach Radar/Checkout), nimmt Kandidaten aus der Queue # und vergleicht sie gegen die BASELINE der Amtsinhaber. # # Modi: # (ohne Argument) Cron-Modus: Baseline fehlt → Baseline messen; sonst ältesten # Kandidaten aus der Queue prüfen; nichts zu tun → still. # --baseline Amtsinhaber über llama-swap (:8080) neu vermessen. # --kandidat einen Kandidaten-Spec (JSON) direkt prüfen. # # Leitplanken (User-Entscheid 17.07.): max 1 Kandidat je Lauf · Download-Deckel 35 GB # (größere Kandidaten → Karte, Download erst nach Klick) · eigener Cache mit # Auto-Aufräumen · Live-Betrieb bleibt unangetastet (eigener Server auf :5899) · # der Prüfstand EMPFIEHLT NUR — Rollen-Wechsel entscheidet der Commander per Karte. # RAM-Wache vor dem Serverstart; Heredoc+Pipe-Falle vermieden (Temp-Dateien, Lehre 10.07.). set -uo pipefail MC="$HOME/mission-control-v2" PSD="$MC/deploy/pruefstand" VAULT="$HOME/wissens-vault" STATE="$HOME/.hermes/state/pruefstand" CACHE="/srv/models/pruefstand-cache" HERMES="$HOME/.local/bin/hermes" BENCH_BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}" BENCH_PORT="${BENCH_PORT:-5899}" LIVE="http://127.0.0.1:8080/v1" JUDGE_MODEL="${PRUEFSTAND_JUDGE:-gpt-oss-120b}" DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-100}" DEADLINE_MIN="${PRUEFSTAND_DEADLINE_MIN:-100}" mkdir -p "$STATE/queue" "$STATE/done" "$STATE/ergebnisse" # Suiten je Rolle (Amtsinhaber-Alias = Rollenname in llama-swap) suiten_fuer() { case "$1" in hermes) echo "speed,agent,recherche,deutsch" ;; coder) echo "speed,coding,agent" ;; heavy) echo "speed,coding,recherche,deutsch" ;; vision) echo "speed,vision" ;; scout) echo "speed,agent,vision" ;; *) echo "speed,recherche,deutsch" ;; esac } briefkasten() { # $1=Betreff $2=Text (stiller Chronik-Spiegel, source=pruefstand) curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \ -H 'Content-Type: application/json' \ --data "$(python3 - "$1" "$2" <<'PY' import json, sys print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1], "source": "pruefstand", "priority": "silent"})) PY )" >/dev/null 2>&1 || true } engine_build() { # installierte llama.cpp-Build-Nummer (leer, wenn nicht ermittelbar) LD_LIBRARY_PATH="$(dirname "$BENCH_BIN")" "$BENCH_BIN" --version 2>&1 \ | grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true } aggregiere_baseline() { # $1=Engine-Build — baut baseline.json aus den Rollen-Dateien python3 - "$STATE" "${1:-}" <<'PY' import glob, json, os, sys, time state, build = sys.argv[1], sys.argv[2] basis = {} for f in glob.glob(os.path.join(state, "ergebnisse", "baseline-*.json")): rolle = os.path.basename(f)[len("baseline-"):-len(".json")] try: basis[rolle] = json.load(open(f)).get("zusammenfassung", {}) except Exception: pass json.dump({"erhoben": time.strftime("%Y-%m-%d %H:%M"), "engine": build, "rollen": basis}, open(os.path.join(state, "baseline.json"), "w"), ensure_ascii=False, indent=1) print(f"\nBasislinie gespeichert ({len(basis)} Rollen, Engine b{build or '?'}).") PY } # Tempo-Teil der Baseline neu messen (Lücke 2, 17.07.): Engine-Updates (So 04:30) # verschieben das Tempo der Amtsinhaber — die Qualitäts-Zahlen bleiben gültig, aber # Tempo-Vergleiche gegen eine alte Engine wären Geister-Zahlen. speed_refresh() { # $1=neue Engine-Build-Nummer local BUILD="$1" echo "## PRÜFSTAND: Tempo-Baseline-Refresh (Engine jetzt b${BUILD}) am $(date '+%d.%m.%Y %H:%M')" local rolle TMP for rolle in hermes coder heavy vision scout; do TMP="$STATE/ergebnisse/refresh-${rolle}.json" echo "### $rolle" python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" --suites speed \ --out "$TMP" --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \ --deadline-min 10 2>&1 | tail -2 python3 - "$STATE/ergebnisse/baseline-${rolle}.json" "$TMP" <<'PY' import json, sys try: basis = json.load(open(sys.argv[1])) neu = json.load(open(sys.argv[2])) except Exception: raise SystemExit if "speed" in (neu.get("zusammenfassung") or {}): basis.setdefault("zusammenfassung", {})["speed"] = neu["zusammenfassung"]["speed"] basis["einzeln"] = ([e for e in basis.get("einzeln", []) if e.get("suite") != "speed"] + [e for e in neu.get("einzeln", []) if e.get("suite") == "speed"]) json.dump(basis, open(sys.argv[1], "w"), ensure_ascii=False, indent=1) PY rm -f "$TMP" done aggregiere_baseline "$BUILD" briefkasten "Prüfstand" "Tempo-Baseline nach Engine-Wechsel neu gemessen (jetzt b${BUILD})." } zusammenfassung_lesen() { # $1=ergebnis.json → kompakte Zeilen "suite: x/y · Note · tg" python3 - "$1" <<'PY' import json, sys try: z = json.load(open(sys.argv[1])).get("zusammenfassung", {}) except Exception: print("(Ergebnis nicht lesbar)"); raise SystemExit for name, s in z.items(): teile = [f"{s['bestanden']}/{s['gesamt']}"] if s.get("note_mittel"): teile.append(f"Note {s['note_mittel']}") if s.get("tg_mittel"): teile.append(f"tg {s['tg_mittel']} t/s") if s.get("pp_mittel"): teile.append(f"pp {s['pp_mittel']} t/s") print(f" {name}: " + " · ".join(teile)) PY } # ---------------------------------------------------------------- Baseline baseline_lauf() { echo "## PRÜFSTAND: Basislinie der Amtsinhaber (erhoben am $(date '+%d.%m.%Y %H:%M'))" echo "(Echte Prüfungen: Coding mit Unit-Tests, Agenten-Aufgaben, Recherche mit Zitat-Gate, Deutsch-Richter, Tempo.)" local rollen="hermes coder heavy vision scout" for rolle in $rollen; do local out="$STATE/ergebnisse/baseline-${rolle}.json" echo echo "### Amtsinhaber \`$rolle\`" python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" \ --suites "$(suiten_fuer "$rolle")" --out "$out" \ --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \ --deadline-min 30 2>&1 | tail -8 done # embed: nur Funktions- und Latenz-Probe (Suiten ergeben hier keinen Sinn) echo echo "### Amtsinhaber \`embed\`" python3 - "$LIVE" <<'PY' import json, sys, time, urllib.request t0 = time.time() req = urllib.request.Request(sys.argv[1].rstrip("/") + "/embeddings", data=json.dumps({"model": "embed", "input": "Prüfstand-Probe"}).encode(), headers={"Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=60) as r: d = json.load(r) dim = len(d["data"][0]["embedding"]) print(f" ok · Dimension {dim} · {round((time.time()-t0)*1000)} ms") except Exception as e: print(f" FEHLER: {e}") PY # Sammel-Baseline schreiben (eine Datei, die Kandidaten-Läufe vergleichen) aggregiere_baseline "$(engine_build)" briefkasten "Prüfstand" "Basislinie der Amtsinhaber erhoben — künftige Kandidaten werden dagegen verglichen." } # ---------------------------------------------------------------- Kandidat kandidat_lauf() { # $1=spec.json local SPEC="$1" local KEY ROLLE HF_ID WARUM KEY="$(jq -r '.key' "$SPEC")" ROLLE="$(jq -r '.rolle' "$SPEC")" HF_ID="$(jq -r '.hf_id' "$SPEC")" WARUM="$(jq -r '.warum // ""' "$SPEC")" echo "## PRÜFSTAND: Kandidat \`$HF_ID\` für Rolle \`$ROLLE\` (am $(date '+%d.%m.%Y %H:%M'))" [ -n "$WARUM" ] && echo "Anlass: $WARUM" if [ -f "$STATE/done/${KEY}.json" ]; then echo "Schon geprüft (State done/${KEY}) — nichts zu tun." return 0 fi # ---- GGUF im HF-Repo finden (bevorzugt ~Q4, ein Stück, Deckel prüfen) local TREE_JSON="$STATE/tmp-tree-${KEY}.json" curl -sf --max-time 60 \ "https://huggingface.co/api/models/${HF_ID}/tree/main?recursive=true" \ -o "$TREE_JSON" || { echo "HF-API nicht erreichbar — Abbruch, Spec bleibt in der Queue."; return 1; } local AUSWAHL AUSWAHL="$(python3 - "$TREE_JSON" "$DECKEL_GB" <<'PY' import json, re, sys baum = json.load(open(sys.argv[1])) deckel = float(sys.argv[2]) * 2**30 dateien = [(e.get("path",""), (e.get("lfs") or {}).get("size") or e.get("size") or 0) for e in baum if isinstance(e, dict)] ggufs = [(p, g) for p, g in dateien if p.lower().endswith(".gguf")] mmproj = next((p for p, g in ggufs if "mmproj" in p.lower()), "") haupt = [(p, g) for p, g in ggufs if "mmproj" not in p.lower()] # Split-GGUFs (…-00001-of-…) klammern wir in v1 aus — sauberer Einzeldatei-Download haupt = [(p, g) for p, g in haupt if not re.search(r"-\d{5}-of-\d{5}", p)] prio = ["q4_k_m", "ud-q4_k_m", "q4_k_s", "iq4", "q5_k_m", "q4", "q8_0"] def rang(p): pl = p.lower() for i, q in enumerate(prio): if q in pl: return i return len(prio) haupt.sort(key=lambda t: (rang(t[0]), t[1])) if not haupt: print("KEINE"); raise SystemExit pfad, groesse = haupt[0] status = "OK" if groesse and groesse <= deckel else "ZU_GROSS" print(status); print(pfad); print(groesse or 0); print(mmproj) PY )" rm -f "$TREE_JSON" local STATUS PFAD GROESSE MMPROJ STATUS="$(printf '%s\n' "$AUSWAHL" | sed -n 1p)" PFAD="$(printf '%s\n' "$AUSWAHL" | sed -n 2p)" GROESSE="$(printf '%s\n' "$AUSWAHL" | sed -n 3p)" MMPROJ="$(printf '%s\n' "$AUSWAHL" | sed -n 4p)" local GB=$(( ${GROESSE:-0} / 1073741824 )) if [ "$STATUS" = "KEINE" ]; then echo "Kein brauchbares Einzel-GGUF im Repo (nur Splits oder gar keins) — Karte statt Download." "$HERMES" kanban create "Prüfstand: ${HF_ID} braucht Handarbeit (kein Einzel-GGUF)" \ --body "Kandidat für Rolle ${ROLLE}. Im HF-Repo liegt kein Einzeldatei-GGUF ≤ ${DECKEL_GB} GB (vermutlich Split-Dateien). Bei Interesse manuell laden und 'bash ~/mission-control-v2/deploy/pruefstand/pruefstand.sh --kandidat ' fahren. Anlass: ${WARUM}" \ --triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true mv "$SPEC" "$STATE/done/${KEY}.spec.json" echo "{\"status\":\"kein_gguf\"}" > "$STATE/done/${KEY}.json" return 0 fi if [ "$STATUS" = "ZU_GROSS" ]; then echo "GGUF ${PFAD} ist ${GB} GB > Deckel ${DECKEL_GB} GB — Download wartet auf deinen Klick (Karte liegt bereit)." "$HERMES" kanban create "Prüfstand: ${HF_ID} (${GB} GB) — Download freigeben?" \ --body "Kandidat für Rolle ${ROLLE}, Datei ${PFAD} (${GB} GB) liegt ÜBER dem Autonomie-Deckel von ${DECKEL_GB} GB (Entscheid 17.07.). Nach Freigabe: Spec wieder in ~/.hermes/state/pruefstand/queue/ legen und PRUEFSTAND_DECKEL_GB erhöhen oder manuell laden. Anlass: ${WARUM}" \ --triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true mv "$SPEC" "$STATE/done/${KEY}.spec.json" echo "{\"status\":\"zu_gross\",\"gb\":${GB}}" > "$STATE/done/${KEY}.json" briefkasten "Prüfstand" "Kandidat ${HF_ID} (${GB} GB) wartet auf Download-Freigabe (Deckel ${DECKEL_GB} GB)." return 0 fi # ---- Platz- und RAM-Wache local FREI_GB FREI_GB="$(df --output=avail -BG /srv/models | tail -1 | tr -dc '0-9')" if [ "${FREI_GB:-0}" -lt $(( GB + 20 )) ]; then echo "Zu wenig Platz auf /srv/models (${FREI_GB} GB frei, gebraucht ~$((GB+20))) — Lauf verschoben, Spec bleibt in der Queue." return 1 fi # ---- Download in den Cache (mit Resume, Auto-Aufräumen am Ende) mkdir -p "$CACHE/$KEY" local ZIEL="$CACHE/$KEY/$(basename "$PFAD")" echo "Lade ${PFAD} (${GB} GB) …" curl -fL --retry 3 -C - --max-time 7200 -o "$ZIEL" \ "https://huggingface.co/${HF_ID}/resolve/main/${PFAD}" \ || { echo "Download fehlgeschlagen — Spec bleibt in der Queue."; return 1; } local MM_ZIEL="" if [ -n "$MMPROJ" ] && { [ "$ROLLE" = "vision" ] || [ "$ROLLE" = "scout" ]; }; then MM_ZIEL="$CACHE/$KEY/$(basename "$MMPROJ")" curl -fL --retry 3 -C - --max-time 3600 -o "$MM_ZIEL" \ "https://huggingface.co/${HF_ID}/resolve/main/${MMPROJ}" || MM_ZIEL="" fi local MEM_FREI_GB MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)" if [ "${MEM_FREI_GB:-0}" -lt $(( GB + 8 )) ]; then echo "Zu wenig freier RAM (${MEM_FREI_GB} GB) für ein ${GB}-GB-Modell neben dem Warm-Set — Lauf verschoben." return 1 fi # ---- Kandidaten-Server auf dem Bench-Port (Live-Betrieb unangetastet) echo "Starte Kandidaten-Server (:${BENCH_PORT}) …" local MMFLAG="" [ -n "$MM_ZIEL" ] && MMFLAG="--mmproj $MM_ZIEL" $BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \ -c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \ >/tmp/pruefstand-server.log 2>&1 & local SPID=$! trap 'kill $SPID 2>/dev/null; wait $SPID 2>/dev/null' RETURN local BEREIT=0 for i in $(seq 1 240); do curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { BEREIT=1; break; } sleep 2 kill -0 $SPID 2>/dev/null || break done if [ "$BEREIT" != "1" ]; then echo "LADE-CRASH — Kandidat startet nicht (Architektur zu neu für die Engine?):" tail -3 /tmp/pruefstand-server.log echo "{\"status\":\"lade_crash\"}" > "$STATE/done/${KEY}.json" mv "$SPEC" "$STATE/done/${KEY}.spec.json" rm -rf "${CACHE:?}/$KEY" briefkasten "Prüfstand" "Kandidat ${HF_ID} startet auf unserer Engine nicht (Lade-Crash) — aussortiert." return 0 fi # ---- Das volle Programm local OUT="$STATE/ergebnisse/kandidat-${KEY}.json" echo "Fahre Suiten: $(suiten_fuer "$ROLLE") (Zeitbudget ${DEADLINE_MIN} min)" python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \ --model "kandidat" --suites "$(suiten_fuer "$ROLLE")" --out "$OUT" \ --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \ --deadline-min "$DEADLINE_MIN" 2>&1 | tail -8 kill $SPID 2>/dev/null; wait $SPID 2>/dev/null; trap - RETURN # ---- MTP-Fairness (Lücke 1, 17.07.): das Live-Hirn läuft MIT draft-mtp (+26 %) — # ein Kandidat mit eingebautem MTP-Kopf würde ohne Zusatzmessung systematisch # unterschätzt. Erkennung: Dateiname ODER nextn-Tensoren im GGUF-Kopf. if printf '%s' "$PFAD" | grep -qi 'mtp' || head -c 8000000 "$ZIEL" 2>/dev/null | grep -aq 'nextn'; then echo "MTP-Kopf erkannt — Zusatzmessung mit draft-mtp (Fairness gegen das Live-Hirn) …" $BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \ -c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \ --spec-type draft-mtp --spec-draft-n-max 3 \ >/tmp/pruefstand-server-mtp.log 2>&1 & local MPID=$! trap 'kill $MPID 2>/dev/null; wait $MPID 2>/dev/null' RETURN local MBEREIT=0 for i in $(seq 1 120); do curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { MBEREIT=1; break; } sleep 2 kill -0 $MPID 2>/dev/null || break done if [ "$MBEREIT" = "1" ]; then local MOUT="$STATE/ergebnisse/kandidat-${KEY}-mtp.json" python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \ --model "kandidat" --suites speed --out "$MOUT" \ --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" --deadline-min 10 2>&1 | tail -2 python3 - "$OUT" "$MOUT" <<'PY' import json, sys haupt = json.load(open(sys.argv[1])) try: s = (json.load(open(sys.argv[2])).get("zusammenfassung") or {}).get("speed") except Exception: s = None if s: haupt.setdefault("zusammenfassung", {})["speed_mtp"] = s json.dump(haupt, open(sys.argv[1], "w"), ensure_ascii=False, indent=1) PY else echo "Kandidat startet mit draft-mtp NICHT — Zusatzmessung entfällt (im Steckbrief zählt der Basis-Wert)." fi kill $MPID 2>/dev/null; wait $MPID 2>/dev/null; trap - RETURN fi # ---- Steckbrief: Kandidat vs. Amtsinhaber local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md" local SIEGER_FLAG="$STATE/tmp-sieger-${KEY}" rm -f "$SIEGER_FLAG" python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" "$SIEGER_FLAG" <<'PY' import json, sys, time kand = json.load(open(sys.argv[1])) try: basis = json.load(open(sys.argv[2]))["rollen"].get(sys.argv[3], {}) except Exception: basis = {} rolle, hf_id, warum, ziel = sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6] kz = kand.get("zusammenfassung", {}) zeilen = [f"# Prüfstand-Steckbrief: {hf_id}", f"Rolle: **{rolle}** · erhoben {time.strftime('%d.%m.%Y %H:%M')}", f"Anlass: {warum}" if warum else "", "", "| Suite | Kandidat | Amtsinhaber |", "|---|---|---|"] punkte_k = punkte_b = faelle = 0 for name in sorted(set(kz) | set(basis)): def fmt(s): if not s: return "—" t = f"{s['bestanden']}/{s['gesamt']}" if s.get("note_mittel"): t += f" · Note {s['note_mittel']}" if s.get("tg_mittel"): t += f" · tg {s['tg_mittel']}" if s.get("pp_mittel"): t += f" · pp {s['pp_mittel']}" return t zeilen.append(f"| {name} | {fmt(kz.get(name))} | {fmt(basis.get(name))} |") if name in kz and name in basis and not name.startswith("speed"): faelle += 1 punkte_k += kz[name]["bestanden"] / max(1, kz[name]["gesamt"]) punkte_b += basis[name]["bestanden"] / max(1, basis[name]["gesamt"]) # Tempo: best-of speed/speed_mtp — die Baseline ist live MIT MTP gemessen (Fairness) tg_k = max(((kz.get(s) or {}).get("tg_mittel") or 0) for s in ("speed", "speed_mtp")) tg_b = (basis.get("speed") or {}).get("tg_mittel") or 0 zeilen.append("") if faelle: qk, qb = punkte_k / faelle, punkte_b / faelle tempo = (f"Tempo {round(100*tg_k/tg_b-100):+d} % vs. Amtsinhaber" if tg_k and tg_b else "Tempo-Vergleich unvollständig") if qk >= qb and tg_k >= 0.8 * (tg_b or tg_k): urteil = "KANDIDAT SIEHT STARK AUS — Auto-Adoption greift (Etappe E5)." open(sys.argv[7], "w").write("1") elif qk >= qb: urteil = "Qualität hält mit, aber deutlich langsamer — vermutlich kein Aufsteiger." else: urteil = "Qualität unter Amtsinhaber — kein Aufsteiger." zeilen.append(f"**Einordnung:** Bestehensquote {qk:.0%} vs. {qb:.0%} · {tempo}. {urteil}") zeilen.append("") zeilen.append("_Auto-Adoption (Etappe E5): Der Prüfstand tauscht Sieger-Modelle nun selbstständig aus._") text = "\n".join(z for z in zeilen if z is not None) open(ziel, "w", encoding="utf-8").write(text + "\n") print(text) PY # ---- Vault-Bericht + Karte + stille Chronik, dann Cache aufräumen mkdir -p "$VAULT/pruefstand" cp "$BERICHT" "$VAULT/pruefstand/${KEY}.md" 2>/dev/null || true ( cd "$VAULT" 2>/dev/null && git add "pruefstand/${KEY}.md" >/dev/null 2>&1 \ && git commit -q -m "pruefstand: Steckbrief ${KEY}" >/dev/null 2>&1 ) || true "$HERMES" kanban create "Prüfstand-Ergebnis: ${HF_ID} für Rolle ${ROLLE}" \ --body "$(cat "$BERICHT")" \ --triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true briefkasten "Prüfstand" "Kandidat ${HF_ID} (Rolle ${ROLLE}) geprüft — Steckbrief im Auftrags-Kanban und Vault (pruefstand/${KEY}.md)." mv "$SPEC" "$STATE/done/${KEY}.spec.json" 2>/dev/null || true cp "$OUT" "$STATE/done/${KEY}.json" 2>/dev/null || echo "{\"status\":\"geprueft\"}" > "$STATE/done/${KEY}.json" # ---- Auto-Adoption (Etappe E5) if [ -f "$SIEGER_FLAG" ]; then echo "Kandidat hat gewonnen — starte Auto-Adoption für Rolle $ROLLE..." local FINAL_DIR="/srv/models/${HF_ID//\//-}-GGUF" sudo -n mkdir -p "$FINAL_DIR" sudo -n mv "$ZIEL" "$FINAL_DIR/" local FINAL_GGUF="$FINAL_DIR/$(basename "$PFAD")" local FINAL_MMPROJ="" if [ -n "$MM_ZIEL" ]; then sudo -n mv "$MM_ZIEL" "$FINAL_DIR/" FINAL_MMPROJ="$FINAL_DIR/$(basename "$MMPROJ")" fi sudo -n chown -R hitonabi:hitonabi "$FINAL_DIR" 2>/dev/null || true # Config updaten local CONFIG="$MC/deploy/llama-swap.config.yaml" python3 "$PSD/auto_adopt.py" "$CONFIG" "$ROLLE" "$HF_ID" "$FINAL_GGUF" "$FINAL_MMPROJ" # Reload llama-swap (via worker oder systemctl) if systemctl --user is-active --quiet llama-swap; then systemctl --user restart llama-swap briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde erfolgreich durch '$HF_ID' ersetzt (Neustart erledigt)." else briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde durch '$HF_ID' ersetzt (llama-swap war inaktiv)." fi rm -f "$SIEGER_FLAG" fi rm -rf "${CACHE:?}/$KEY" echo echo "(Cache aufgeräumt; Roh-Ergebnisse: $STATE/ergebnisse/kandidat-${KEY}.*)" } # ---------------------------------------------------------------- Einstieg case "${1:-}" in --baseline) baseline_lauf ;; --kandidat) [ -n "${2:-}" ] || { echo "Nutzung: pruefstand.sh --kandidat "; exit 1; } kandidat_lauf "$2" ;; *) if [ ! -f "$STATE/baseline.json" ]; then baseline_lauf exit 0 fi # Engine gewechselt seit der Baseline? → Tempo-Teil neu messen (Geister-Zahlen-Schutz) BUILD="$(engine_build)" BASE_BUILD="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("engine") or "")' "$STATE/baseline.json" 2>/dev/null || true)" if [ -n "$BUILD" ] && [ "$BUILD" != "$BASE_BUILD" ]; then speed_refresh "$BUILD" fi NAECHSTER="$(ls -1tr "$STATE/queue/"*.json 2>/dev/null | head -1 || true)" if [ -n "$NAECHSTER" ]; then kandidat_lauf "$NAECHSTER" fi # keine Queue + keine Engine-Drift → LEERER stdout → --no-agent-Cron bleibt still ;; esac