Feat: Frontend-Overhaul E — Lane-Editor, Routing-Policy (hot-reload) & Latenz-Karte

Teil 1: VoiceLatencyCard auf dem Dashboard (GET /api/voice/metrics, C2) —
zeigt STT/Vision/Chat-TTFB/TTS mit p50/p95/last + count.

Teil 2: UI-editierbare Routing-Policy. Neuer routing_policy.py (hot-reload JSON
unter MODELS_DIR/mc2-routing.json, Env=Defaults, atomarer Write, Validierung).
router_logic, gateway_proxy und gateway.routing_summary lesen jetzt live via
load_policy(); routing_summary ist lane-bewusst (chat/coding statt altem auto).
Neue Endpoints GET/PUT /api/routing/policy.

Teil 3: LaneEditor.tsx als ZONE im Cockpit (chat/coding-Aliase + Schwellen +
fast_no_think, Speichern/Default-je-Feld); Gateway-Node zeigt die Lanes.

Verifiziert: npm run build (tsc strict) clean, FastAPI TestClient (GET/PUT,
Validierung, Persistenz, Hot-reload durch die API), venv-Smoke (Routing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-29 18:58:17 +02:00
parent 3611defe5d
commit 9e432dbd2d
15 changed files with 876 additions and 367 deletions
+17 -23
View File
@@ -6,26 +6,18 @@ Zwei virtuelle Lanes, die Clients/IDEs auswählen — der Router pickt das echte
- **coding**: Code-Arbeit → `coder` (Qwen3-Coder-Next); riesiger/architektonischer Kontext → `heavy`;
triviale Kurzfrage ohne Code → `fast` (Tempo).
Regelbasiert, sub-ms, ohne Cloud. Schwellen/Aliases via Env überschreibbar (Phase 2: UI-editierbare
Policy-JSON). Lucy läuft NICHT hierüber — die ist der Hermes-Agent (:8642), eigene Ebene.
Regelbasiert, sub-ms, ohne Cloud. Schwellen/Aliases liegen in einer UI-editierbaren Policy
(routing_policy.py, hot-reload; Env = Defaults). Lucy läuft NICHT hierüber — die ist der
Hermes-Agent (:8642), eigene Ebene.
"""
import os
import re
# Echte Modell-Aliases hinter den Lanes (Env-überschreibbar).
FAST = os.environ.get("MC_ROUTE_FAST", "fast")
HEAVY = os.environ.get("MC_ROUTE_HEAVY", "heavy")
CODER = os.environ.get("MC_ROUTE_CODER", "coder")
from services.routing_policy import load_policy
# Schwellen (Zeichen).
HEAVY_CHARS = int(os.environ.get("MC_GATEWAY_HEAVY_CHARS", "8000")) # chat → heavy
# coding-Lane: leichter/schneller Coder als Default (wenn gesetzt = Phase 2b), starker Coder als Eskalation.
CODER_LITE = os.environ.get("MC_ROUTE_CODER_LITE", "").strip() # z.B. "coder-lite"
CODING_ESCALATE_CHARS = int(os.environ.get("MC_CODING_ESCALATE_CHARS", "120000")) # darüber → starker Coder
# Thinking auf der fast-Spur aus → flotte Alltags-Antworten (Qwen3.6 ist ein Reasoning-Modell).
FAST_NO_THINK = os.environ.get("MC_FAST_NO_THINK", "1") not in ("0", "false", "")
# Modell-Aliases & Zeichen-Schwellen liegen jetzt in der UI-editierbaren Policy
# (routing_policy.py) und kommen pro Request via load_policy() (hot-reload). Die Env-Vars
# sind dort die Defaults. Die Regex-Keyword-Listen unten bleiben bewusst im Code.
# Virtuelle Lanes, die im Gateway als „Modelle" sichtbar sind.
LANES = ["coding", "chat"]
@@ -64,21 +56,23 @@ def _text_of(body: dict) -> str:
def _route_chat(text: str, n: int) -> tuple[str, str]:
if n > HEAVY_CHARS:
return HEAVY, f"langer Kontext ({n} > {HEAVY_CHARS} Zeichen)"
p = load_policy()
if n > p["heavy_chars"]:
return p["heavy"], f"langer Kontext ({n} > {p['heavy_chars']} Zeichen)"
if _HEAVY_KW.search(text):
return HEAVY, "Komplexitäts-Schlüsselwort erkannt"
return FAST, "Standard"
return p["heavy"], "Komplexitäts-Schlüsselwort erkannt"
return p["fast"], "Standard"
def _route_coding(text: str, n: int) -> tuple[str, str]:
# Agentisches Coden (OpenCode/RooCode/…) bleibt IMMER beim dedizierten Coder — NIE heavy/fast
# (das sind Allzweck-Modelle, schwächer bei Code). Die Qwen-Coder packen 256K1M Kontext selbst,
# langer Repo-Kontext ist bei Agenten der Normalfall und darf NICHT zu heavy umrouten.
# (Phase 2b: warme schnelle Coder-Stufe CODER_LITE als Default + CODER als Eskalation.)
if CODER_LITE and not _CODING_HEAVY_KW.search(text) and n <= CODING_ESCALATE_CHARS:
return CODER_LITE, "Coding (schneller Coder)"
return CODER, "Coding -> starker Coder"
# (Phase 2b: warme schnelle Coder-Stufe coder_lite als Default + coder als Eskalation.)
p = load_policy()
if p["coder_lite"] and not _CODING_HEAVY_KW.search(text) and n <= p["coding_escalate_chars"]:
return p["coder_lite"], "Coding (schneller Coder)"
return p["coder"], "Coding -> starker Coder"
def choose_for_lane(lane: str, body: dict) -> tuple[str, str]: