Ampel / ampel (push) Successful in 26s
Ballast raus: - 35 Routen ohne Nutzer entfernt (agent/*, fit, roles, ctx, drafts, groups, routing/policy, system/history, system/self-update, maintenance/reboot, zeitmaschine/inhalt, zeitplan, voice/health|metrics|trace|voices|reference|tts). Von 95 auf 60. - Tote Module geloescht: agent-Router, roles, agent_aktivitaet, metrics_history (samt 10-s-Sampler), voice_metrics, migrate_config, parse_mc2_timeout, scripts/. - Unbenutzte Funktionen und Konstanten entfernt (Modell-Upgrade-Empfehlung, Draft-/Kontext- Setzer, Konsole, PC-Ausfuehrer-Probe, Routing-Policy-Editor ...). Robuster: - Jobs in eigener Prozessgruppe (Abbrechen beendet wirklich alles), Zeitlimit je Job-Art, start_job_exklusiv: zwei Klicks starten kein doppeltes Update mehr; alte Jobs raeumen sich auf. - Update-Pruefung meldet Fehler (pruef_fehler, Lampe "Pruefung unklar") statt "aktuell". - Nach jedem Update sofort neu pruefen (update_stand) statt 10 Minuten alten Stand zeigen. - llama-swap-Config: Sperre (RLock + flock) fuer UI, Radar, Aufraeumen und Hirn-Umstellung. - Hermes-Config: bei Lesefehler nichts schreiben, atomar, mit Sicherung. - Live-Strom und Gateway-Warnung blockieren den Event-Loop nicht mehr (Lucy, OpenChamber). - Gateway antwortet bei Engine-Ausfall im OpenAI-Fehlerformat (502) statt nacktem 500. - Abgestuerzte Waechter-Pruefung wird ein gelber Hinweis statt still zu verschwinden. - Download laedt nur den gewuenschten Quant (vorher bei Fehlen alle Teile aller Varianten), Download-Jobs in Gruppe "download"; HF-Suche kodiert den Suchbegriff. - Herkunftspruefung: schreibende /api-Aufrufe fremder Webseiten werden abgelehnt (keine Anmeldung, User-Entscheid); Skripte, Desktop-Lucy und /v1 unveraendert. - Modellpfade: Eintragen und Loeschen nur innerhalb von MODELS_DIR. - Dienste-Liste fragt keine abgebauten Dienste mehr ab (PC-Ausfuehrer haette 3 s gekostet). - SSE-Fehlerzeilen von /api/voice/chat als gueltiges JSON. - mission-control-2.service: --timeout-graceful-shutdown 3 (Neustart ohne 10-s-Haenger). Tests: 92 gruen (neu: Herkunft, Quant-Auswahl, abgestuerzte Pruefung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.8 KiB
Python
118 lines
4.8 KiB
Python
"""
|
|
UI-editierbare Routing-Policy für die Gateway-Lanes (coding/chat).
|
|
|
|
Persistiert als JSON unter MC_ROUTING_POLICY_PATH (Default MODELS_DIR/mc2-routing.json —
|
|
gleiche Konvention wie mc2-discover.json). **Hot-reload:** load_policy() liest die Datei nur
|
|
bei Änderung neu (mtime-Cache) → UI-Edits greifen ohne Dienst-Neustart. Die Env-Vars (bisher
|
|
einzige Stellschraube in router_logic.py) bleiben als Defaults/Fallback erhalten.
|
|
|
|
Bewusst NICHT editierbar (v1): die Regex-Keyword-Listen (heavy/coding-heavy/code-hint) — die
|
|
bleiben in router_logic.py im Code.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
from config import MODELS_DIR
|
|
|
|
POLICY_PATH = Path(os.environ.get("MC_ROUTING_POLICY_PATH", str(MODELS_DIR / "mc2-routing.json")))
|
|
|
|
|
|
def _env_bool(name: str, default: str) -> bool:
|
|
return os.environ.get(name, default) not in ("0", "false", "")
|
|
|
|
|
|
# Defaults aus den Env-Vars — Quelle der Wahrheit, solange keine Policy-Datei existiert.
|
|
DEFAULTS: dict = {
|
|
"fast": os.environ.get("MC_ROUTE_FAST", "fast"),
|
|
"heavy": os.environ.get("MC_ROUTE_HEAVY", "heavy"),
|
|
"coder": os.environ.get("MC_ROUTE_CODER", "coder"),
|
|
"coder_lite": os.environ.get("MC_ROUTE_CODER_LITE", ""),
|
|
# Bild-Weiche (Faden 11): Requests mit Bild-Anhang werden automatisch hierhin geroutet
|
|
# (die MTP-Hirn-Config kann keine Bilder). "" schaltet die Weiche ab (Passthrough).
|
|
"vision": os.environ.get("MC_ROUTE_VISION", "vision"),
|
|
# Seit 24.09.2026: Bild-Zwilling des Coders — Coder-Anfragen mit neuem Bild gehen hierhin statt an vision.
|
|
# "" = auch Coder-Bilder gehen an vision.
|
|
"coder_vision": os.environ.get("MC_ROUTE_CODER_VISION", "coder-bild"),
|
|
"heavy_chars": int(os.environ.get("MC_GATEWAY_HEAVY_CHARS", "8000")),
|
|
"coding_escalate_chars": int(os.environ.get("MC_CODING_ESCALATE_CHARS", "120000")),
|
|
"fast_no_think": _env_bool("MC_FAST_NO_THINK", "1"),
|
|
}
|
|
|
|
# Feld-Spezifikation für die UI (Typ + Grenzen + Label). Treibt Editor & Validierung.
|
|
FIELDS: list[dict] = [
|
|
{"key": "fast", "label": "fast-Alias (chat: Standard)", "type": "str"},
|
|
{"key": "heavy", "label": "heavy-Alias (chat: lang/komplex)", "type": "str"},
|
|
{"key": "coder", "label": "coder-Alias (coding: stark / Eskalation)", "type": "str"},
|
|
{"key": "coder_lite", "label": "coder-lite-Alias (coding: schneller Default; leer = aus)", "type": "str"},
|
|
{"key": "vision", "label": "vision-Alias (Bild-Weiche: Requests mit Bild; leer = aus)", "type": "str"},
|
|
{"key": "coder_vision", "label": "Bild-Zwilling des Coders (leer = Coder-Bilder an vision)", "type": "str"},
|
|
{"key": "heavy_chars", "label": "chat → heavy ab N Zeichen", "type": "int", "min": 500, "max": 1_000_000},
|
|
{"key": "coding_escalate_chars", "label": "coding → starker Coder ab N Zeichen", "type": "int", "min": 1000, "max": 4_000_000},
|
|
{"key": "fast_no_think", "label": "fast-Spur: Thinking aus (flotte Antworten)", "type": "bool"},
|
|
]
|
|
|
|
_LOCK = threading.Lock()
|
|
_CACHE: dict = {"mtime": None, "policy": None}
|
|
|
|
|
|
def _read_file() -> dict:
|
|
try:
|
|
with open(POLICY_PATH, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return data if isinstance(data, dict) else {}
|
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
return {}
|
|
|
|
|
|
def _coerce(patch: dict) -> dict:
|
|
"""Nur bekannte Keys, typ-/bereichsvalidiert. Wirft ValueError bei ungültigen Werten."""
|
|
spec = {f["key"]: f for f in FIELDS}
|
|
out: dict = {}
|
|
for k, v in (patch or {}).items():
|
|
f = spec.get(k)
|
|
if not f:
|
|
continue # unbekannte Keys still verwerfen
|
|
if f["type"] == "int":
|
|
iv = int(v)
|
|
lo, hi = f.get("min", 1), f.get("max", 10**9)
|
|
if not (lo <= iv <= hi):
|
|
raise ValueError(f"{k}={iv} außerhalb [{lo}, {hi}]")
|
|
out[k] = iv
|
|
elif f["type"] == "bool":
|
|
out[k] = bool(v)
|
|
else: # str
|
|
sv = str(v).strip()
|
|
if k not in ("coder_lite", "vision", "coder_vision") and not sv:
|
|
raise ValueError(f"{k} darf nicht leer sein")
|
|
out[k] = sv
|
|
return out
|
|
|
|
|
|
def _coerce_safe(patch: dict) -> dict:
|
|
"""Wie _coerce, aber schluckt Fehler — kaputte Datei darf den Betrieb nicht stoppen."""
|
|
try:
|
|
return _coerce(patch)
|
|
except (ValueError, TypeError):
|
|
return {}
|
|
|
|
|
|
def load_policy() -> dict:
|
|
"""Aktuelle Policy (Datei über DEFAULTS gemerged). Hot-reload via mtime-Cache, pro Request billig."""
|
|
try:
|
|
mtime = POLICY_PATH.stat().st_mtime
|
|
except OSError:
|
|
mtime = None
|
|
with _LOCK:
|
|
if _CACHE["policy"] is None or _CACHE["mtime"] != mtime:
|
|
merged = {**DEFAULTS}
|
|
if mtime is not None:
|
|
merged.update(_coerce_safe(_read_file()))
|
|
_CACHE["mtime"] = mtime
|
|
_CACHE["policy"] = merged
|
|
return dict(_CACHE["policy"])
|
|
|
|
|