bilder: Hirn und Coder sehen selbst - ueber Bild-Zwillinge, Text bleibt mit Draft schnell
Ampel / ampel (push) Failing after 21s
Ampel / ampel (push) Failing after 21s
llama.cpp kann Draft-Beschleunigung und Bilder nicht zusammen (HTTP 500 "failed to process speculative batch", b11057 und b11157 geprueft; speculative.n_max=0 je Anfrage hilft nicht). Darum bekommen Hirn und Coder je einen Bild-Zwilling: gleiche Gewichte plus Projektor, ohne Draft (vision, coder-bild), in einer eigenen llama-swap-Gruppe, die den Coder nicht verdraengt. Probe 24.09.: beide 8/8 Bildmerkmale; Hirn-Zwilling 68 t/s, Coder-Zwilling 12,5 t/s. Bild-Weiche v3 im Gateway: Bild im aktuellen Schritt geht an den Zwilling der Rolle, aeltere Bilder werden einmal beschrieben (gemerkt) und als Text mitgeschickt, damit der Rest einer Agenten-Aufgabe wieder beim schnellen Modell laeuft. Qwen3-VL gibt "vision" ab, der Coder verliert den Projektor, der mit Draft nur HTTP 500 lieferte. Radar misst die Bildfaehigkeit des heutigen Modells ueber dessen Zwilling (sonst gewaenne jeder bildfaehige Kandidat mit "versteht Bilder"). Pruefstand: Coder darf vor dem Aendern lesen (Version 3). Modelle-Seite zeigt "Bilder: ja" ueber den Zwilling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2cc1e1cc47
commit
91aa16eee1
@@ -1,12 +1,15 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
import httpx
|
||||
from config import LLAMA_SWAP_URL
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from services.gateway_stream import record_stream_chunk, record_usage, warn_truncation
|
||||
from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image
|
||||
from services.router_logic import VISION_CAPABLE, bild_ziel, bilder_aufteilen, choose_for_lane, has_image
|
||||
from services.routing_policy import load_policy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -22,15 +25,15 @@ _LANG_DIRECTIVE = os.environ.get(
|
||||
"Quellcode, Bezeichner und Shell-Befehle bleiben unverändert.")
|
||||
|
||||
|
||||
# Deckel für die Bild-Weiche. Ohne ihn wächst die Wartezeit linear mit der Bildzahl:
|
||||
# Deckel für die Bild-Beschreibungen. Ohne ihn wächst die Wartezeit linear mit der Bildzahl:
|
||||
# je Bild bis zu 2 Versuche à _BILD_TIMEOUT_S, und der Client hängt so lange am offenen
|
||||
# Request. Über _BILD_MAX Bilder wird gar nicht erst beschrieben — der Aufrufer fällt
|
||||
# dann auf die normale Vision-Umleitung zurück (ehrlich langsam statt scheinbar hängend).
|
||||
# Request. Sind mehr als _BILD_MAX Bilder NEU zu beschreiben, bleiben alle Bilder drin und die
|
||||
# Anfrage geht an den Bild-Zwilling (ehrlich langsam statt scheinbar hängend).
|
||||
_BILD_TIMEOUT_S = float(os.environ.get("MC_CODER_IMAGE_TIMEOUT_S", "240"))
|
||||
_BILD_MAX = int(os.environ.get("MC_CODER_IMAGE_MAX", "4"))
|
||||
|
||||
# Bild-Beschreibung für Coder-Ziele: Prompt bewusst auf wörtliche Wiedergabe von
|
||||
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
||||
# Beschreibung älterer Bilder: Prompt bewusst auf wörtliche Wiedergabe von Code/Fehlermeldungen
|
||||
# getrimmt — das schnelle Modell arbeitet in den Folgeschritten nur noch mit diesem Text.
|
||||
_BILD_BESCHREIB_PROMPT = os.environ.get(
|
||||
"MC_CODER_IMAGE_PROMPT",
|
||||
"Beschreibe dieses Bild vollstaendig und praezise auf Deutsch: sichtbarer Text, "
|
||||
@@ -70,61 +73,74 @@ def _apply_no_think_marker(body: dict) -> None:
|
||||
body["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
|
||||
|
||||
def _ist_coder_alias(alias: str) -> bool:
|
||||
"""Coder-Ziele (coder, rohe Qwen-Coder-IDs) bekommen Bild-BESCHREIBUNGEN statt der
|
||||
stumpfen Vision-Umleitung — die Code-Frage bleibt beim Spezialisten."""
|
||||
return "coder" in (alias or "").lower()
|
||||
# Bild-Weiche v3 (24.09.2026). llama.cpp kann Draft-Beschleunigung und Bilder nicht zusammen (HTTP 500
|
||||
# „failed to process speculative batch“, b11057 und b11157 geprüft). Darum haben Hirn und Coder je einen
|
||||
# Bild-Zwilling: dieselben Gewichte mit Bild-Projektor, ohne Draft (vision, coder-bild).
|
||||
# • Bild im aktuellen Schritt → der Zwilling der Rolle sieht es selbst, auch mitten in einer Agenten-Aufgabe.
|
||||
# • Bild aus früheren Schritten → einmal von vision beschrieben (je Bild gemerkt) und als Text mitgeschickt,
|
||||
# damit der Rest der Aufgabe wieder beim schnellen Modell mit Draft läuft.
|
||||
_BESCHREIBUNGEN: OrderedDict[str, str] = OrderedDict()
|
||||
_BESCHREIBUNGEN_MAX = 64
|
||||
|
||||
|
||||
async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str) -> bool:
|
||||
"""Ersetzt jeden Bild-Part durch eine Text-Beschreibung vom Vision-Modell.
|
||||
def _bild_schluessel(part: dict) -> str:
|
||||
return hashlib.sha1(json.dumps(part, sort_keys=True).encode("utf-8")).hexdigest()
|
||||
|
||||
Idee aus einem verwaisten, nie verdrahteten Patch in der Hermes-Quelle
|
||||
(gateway/platforms/api_server.py, 07/2026) — bei der Projekt-Review 15.07.
|
||||
regelkonform hierher umgezogen (Archiv: docs/archiv/). True = alle Bilder
|
||||
ersetzt; False = eine Analyse scheiterte, Aufrufer nutzt die normale
|
||||
Vision-Umleitung als Fallback.
|
||||
"""
|
||||
fundstellen: list[tuple[dict, int, dict]] = []
|
||||
for m in body.get("messages") or []:
|
||||
if not isinstance(m, dict) or not isinstance(m.get("content"), list):
|
||||
continue
|
||||
for i, part in enumerate(m["content"]):
|
||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||
fundstellen.append((m, i, part))
|
||||
if len(fundstellen) > _BILD_MAX:
|
||||
log.warning("Bild-Weiche v2: %s Bilder (Deckel %s) — Request geht an das Vision-Modell "
|
||||
"statt einzeln beschrieben zu werden", len(fundstellen), _BILD_MAX)
|
||||
|
||||
async def _beschreibe(part: dict, vision_alias: str) -> str:
|
||||
"""Beschreibung eines Bild-Parts; Hermes und OpenCode schicken den ganzen Verlauf mit jedem Schritt
|
||||
neu, darum wird jedes Bild nur einmal beschrieben."""
|
||||
schluessel = _bild_schluessel(part)
|
||||
if schluessel in _BESCHREIBUNGEN:
|
||||
_BESCHREIBUNGEN.move_to_end(schluessel)
|
||||
return _BESCHREIBUNGEN[schluessel]
|
||||
frage = {
|
||||
"model": vision_alias,
|
||||
"stream": False,
|
||||
"max_tokens": 700,
|
||||
# Denken aus: sonst frisst die Denkphase das Token-Budget und die Beschreibung bleibt leer.
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
"messages": [{"role": "user", "content": [part, {"type": "text", "text": _BILD_BESCHREIB_PROMPT}]}],
|
||||
}
|
||||
text = ""
|
||||
# Eigener Kurzzeit-Client statt des gepoolten (Befund 15.07.: ReadTimeout nach Sekunden trotz
|
||||
# timeout=240 — llama-swap kappt beim Modell-Swap gern alte Keep-Alive-Sockets) + 1 Retry.
|
||||
for versuch in (1, 2):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=_BILD_TIMEOUT_S) as c:
|
||||
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
||||
if r.status_code == 200:
|
||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||
if text.strip():
|
||||
break
|
||||
log.warning("Bild-Weiche (Versuch %s): Beschreibung leer/HTTP %s — %.200s",
|
||||
versuch, r.status_code, r.text)
|
||||
except Exception:
|
||||
log.warning("Bild-Weiche (Versuch %s): Beschreibung scheiterte", versuch, exc_info=True)
|
||||
text = text.strip()
|
||||
if text:
|
||||
_BESCHREIBUNGEN[schluessel] = text
|
||||
while len(_BESCHREIBUNGEN) > _BESCHREIBUNGEN_MAX:
|
||||
_BESCHREIBUNGEN.popitem(last=False)
|
||||
return text
|
||||
|
||||
|
||||
async def _alte_bilder_beschreiben(alt: list[tuple[dict, int]], vision_alias: str) -> bool:
|
||||
"""Bilder aus früheren Schritten durch ihre Beschreibung ersetzen. False = ging nicht (zu viele neue
|
||||
oder eine Beschreibung scheiterte) — dann bleiben alle Bilder drin."""
|
||||
neu = {_bild_schluessel(msg["content"][idx]) for msg, idx in alt} - set(_BESCHREIBUNGEN)
|
||||
if len(neu) > _BILD_MAX:
|
||||
log.warning("Bild-Weiche: %s ältere Bilder neu zu beschreiben (Deckel %s) — Anfrage geht mit allen "
|
||||
"Bildern an den Bild-Zwilling", len(neu), _BILD_MAX)
|
||||
return False
|
||||
for nr, (msg, idx, part) in enumerate(fundstellen, start=1):
|
||||
frage = {
|
||||
"model": vision_alias,
|
||||
"stream": False,
|
||||
"max_tokens": 700,
|
||||
"messages": [{"role": "user",
|
||||
"content": [part, {"type": "text", "text": _BILD_BESCHREIB_PROMPT}]}],
|
||||
}
|
||||
# Eigener Kurzzeit-Client statt des gepoolten (Befund 15.07.: ReadTimeout nach
|
||||
# Sekunden trotz timeout=240 — llama-swap kappt beim Modell-Swap gern alte
|
||||
# Keep-Alive-Sockets, der Pool reicht sie trotzdem wieder aus) + 1 Retry.
|
||||
text = ""
|
||||
for versuch in (1, 2):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=_BILD_TIMEOUT_S) as c:
|
||||
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
||||
if r.status_code == 200:
|
||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||
if text.strip():
|
||||
break
|
||||
log.warning("Bild-Weiche v2 (Versuch %s): Beschreibung leer/HTTP %s — %.200s",
|
||||
versuch, r.status_code, r.text)
|
||||
except Exception:
|
||||
log.warning("Bild-Weiche v2 (Versuch %s): Vision-Aufruf scheiterte", versuch, exc_info=True)
|
||||
if not text.strip():
|
||||
texte = []
|
||||
for msg, idx in alt:
|
||||
text = await _beschreibe(msg["content"][idx], vision_alias)
|
||||
if not text:
|
||||
return False
|
||||
msg["content"][idx] = {"type": "text", "text": (
|
||||
f"[Bild {nr}: dem Request lag ein Bild bei — {vision_alias} beschreibt es so:\n"
|
||||
f"{text.strip()}]")}
|
||||
texte.append((msg, idx, text))
|
||||
for msg, idx, text in texte:
|
||||
msg["content"][idx] = {"type": "text", "text": f"[Bild aus einem früheren Schritt — so sah es aus:\n{text}]"}
|
||||
return True
|
||||
|
||||
|
||||
@@ -233,25 +249,27 @@ async def _proxy(path: str, request: Request):
|
||||
|
||||
pol = load_policy()
|
||||
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
||||
# Bild-Weiche (Faden 11): Requests mit Bild-Anhang automatisch ans Vision-Modell umleiten,
|
||||
# sofern das Ziel nicht ohnehin bildfähig ist. Das MTP-Hirn (fast/hermes) kann keine Bilder —
|
||||
# so bleibt Lucy schnell, ohne dass jemand manuell das Modell wechselt (Entscheid Weg A).
|
||||
# Coder-Sonderweg (15.07.): Coder-Ziele behalten den Request — die Bilder werden vorab vom
|
||||
# Vision-Modell BESCHRIEBEN und als Text injiziert (Screenshot-Debugging bleibt beim Coder).
|
||||
angefragt = alias
|
||||
# Bild-Weiche v3 (siehe oben): Bild im aktuellen Schritt → Bild-Zwilling der Rolle; ältere Bilder →
|
||||
# Beschreibung als Text, die Anfrage bleibt beim schnellen Modell.
|
||||
vision_alias = pol.get("vision")
|
||||
if vision_alias and alias not in VISION_CAPABLE and has_image(body):
|
||||
if _ist_coder_alias(alias) and await _bilder_fuer_coder_beschreiben(body, client, vision_alias):
|
||||
routed = {"x-mc-routed-to": alias,
|
||||
"x-mc-route-reason": "Bild beschrieben (Vision) -> bleibt beim Coder",
|
||||
"x-mc-lane": routed.get("x-mc-lane", "-")}
|
||||
else:
|
||||
alias = vision_alias
|
||||
frisch, alt = bilder_aufteilen(body)
|
||||
beschrieben = bool(alt) and await _alte_bilder_beschreiben(alt, vision_alias)
|
||||
lane = routed.get("x-mc-lane", "-")
|
||||
if frisch or not beschrieben:
|
||||
alias = bild_ziel(alias, vision_alias, pol.get("coder_vision") or None)
|
||||
body["model"] = alias
|
||||
# HTTP-Header-Werte muessen latin-1 sein — kein '→' o.ae. (sonst 500, 13.07.).
|
||||
routed = {"x-mc-routed-to": alias, "x-mc-route-reason": "Bild erkannt -> Vision-Modell",
|
||||
"x-mc-lane": routed.get("x-mc-lane", "-")}
|
||||
# fast-Spur: Thinking aus für flotte Antworten (sofern Client es nicht selbst setzt).
|
||||
if pol["fast_no_think"] and alias == pol["fast"] and "chat_template_kwargs" not in body:
|
||||
routed = {"x-mc-routed-to": alias, "x-mc-route-reason": "Bild im aktuellen Schritt -> Bild-Zwilling",
|
||||
"x-mc-lane": lane}
|
||||
else:
|
||||
routed = {"x-mc-routed-to": alias,
|
||||
"x-mc-route-reason": "aeltere Bilder beschrieben -> bleibt beim schnellen Modell",
|
||||
"x-mc-lane": lane}
|
||||
# fast-Spur: Thinking aus für flotte Antworten (sofern Client es nicht selbst setzt) — auch wenn die
|
||||
# Anfrage wegen eines Bildes beim Hirn-Zwilling landet (gleiches Hirn, gleiche Spur).
|
||||
if pol["fast_no_think"] and angefragt == pol["fast"] and "chat_template_kwargs" not in body:
|
||||
body["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
_inject_language(body, alias)
|
||||
url = f"{LLAMA_SWAP_URL}{path}"
|
||||
|
||||
@@ -101,6 +101,7 @@ HF_BASIS = "https://huggingface.co"
|
||||
|
||||
ROLLEN = ("hirn", "coder")
|
||||
ALIAS = {"hirn": "hermes", "coder": "coder"} # llama-swap-Alias der Rolle heute
|
||||
ZWILLING = {"hirn": "vision", "coder": "coder-bild"} # Bild-Zwilling der Rolle (seit 24.09.)
|
||||
DISCOVER_ROLLEN = {"coder": "coder", "fast": "hirn", "hermes": "hirn"}
|
||||
PARALLEL = {"hirn": 2, "coder": 1} # Slots im Betrieb, wie heute
|
||||
KANDIDAT_FELDER = ("id", "name", "rolle", "repo", "datei", "groesse_gb", "passt", "eng", "quelle", "status",
|
||||
@@ -1068,14 +1069,34 @@ class _Nachtwaechter(threading.Thread):
|
||||
self._halt.set()
|
||||
|
||||
|
||||
def _bild_zwilling(rolle: str, heute: dict) -> dict | None:
|
||||
"""Der Bild-Zwilling der Rolle: trägt ZWILLING[rolle], hat einen Projektor und dieselben Gewichte."""
|
||||
pfad = heute.get("gguf_path")
|
||||
try:
|
||||
modelle = llamaswap.list_models()
|
||||
except Exception:
|
||||
return None
|
||||
for m in modelle:
|
||||
if (ZWILLING[rolle] in {str(a).lower() for a in m.get("aliases") or []}
|
||||
and "--mmproj" in str(m.get("cmd") or "") and pfad and m.get("gguf_path") == pfad):
|
||||
return m
|
||||
return None
|
||||
|
||||
|
||||
def _baseline(rolle: str, frist: float) -> dict | None:
|
||||
"""Werte des heutigen Modells der Rolle (Cache, höchstens monatlich neu gemessen)."""
|
||||
"""Werte des heutigen Modells der Rolle (Cache, höchstens monatlich neu gemessen). Die Bild-Probe
|
||||
macht der Bild-Zwilling, wenn es einen gibt — so zählt „versteht Bilder“ nicht als Vorteil eines
|
||||
Kandidaten, obwohl das heutige Modell über seinen Zwilling längst sieht."""
|
||||
heute = _heutige_modelle().get(rolle)
|
||||
if not heute:
|
||||
return None
|
||||
befehl = str(heute.get("cmd") or "")
|
||||
kennung = f"{heute['name']}|{hashlib.sha1(befehl.encode()).hexdigest()[:10]}"
|
||||
return _pruefstand().baseline(rolle, str(BASELINE_PATH), kennung=kennung, bild="--mmproj" in befehl,
|
||||
zwilling = _bild_zwilling(rolle, heute)
|
||||
zwilling_befehl = str((zwilling or {}).get("cmd") or "")
|
||||
kennung = f"{heute['name']}|{hashlib.sha1((befehl + zwilling_befehl).encode()).hexdigest()[:10]}"
|
||||
return _pruefstand().baseline(rolle, str(BASELINE_PATH), kennung=kennung,
|
||||
bild="--mmproj" in befehl or zwilling is not None,
|
||||
bild_modell=ZWILLING[rolle] if zwilling else None,
|
||||
frist=frist, max_alter_tage=BASELINE_TAGE, modell=ALIAS[rolle],
|
||||
protokoll=_protokoll)
|
||||
|
||||
|
||||
@@ -51,10 +51,43 @@ _CODE_HINT = re.compile(
|
||||
|
||||
|
||||
# Bild-Weiche (Faden 11): Aliase, die selbst Bilder koennen — die werden NIE umgeroutet.
|
||||
VISION_CAPABLE = {"vision", "scout"}
|
||||
# Seit 24.09.2026 sind vision und coder-bild die Bild-Zwillinge von Hirn und Coder (gleiche Gewichte
|
||||
# plus Bild-Projektor, ohne Draft — llama.cpp kann Draft und Bild nicht zusammen, HTTP 500).
|
||||
VISION_CAPABLE = {"vision", "coder-bild", "scout"}
|
||||
IMAGE_PART_TYPES = {"image_url", "input_image", "image"}
|
||||
|
||||
|
||||
def ist_coder_alias(alias: str) -> bool:
|
||||
"""Coder-Ziele: coder, heavy (derselbe Coder) und rohe Coder-IDs."""
|
||||
a = (alias or "").lower()
|
||||
return "coder" in a or a == "heavy"
|
||||
|
||||
|
||||
def bilder_aufteilen(body: dict) -> tuple[list[tuple[dict, int]], list[tuple[dict, int]]]:
|
||||
"""Bild-Parts in (frisch, alt) teilen. Frisch = nach der letzten Antwort des Modells (der Schritt,
|
||||
um den es gerade geht: neue Nutzer-Nachricht oder Werkzeug-Ergebnis). Alt = davor — das Modell hat
|
||||
sie schon gesehen und seine Schlüsse im Verlauf. Jeder Eintrag ist (Nachricht, Index im content)."""
|
||||
msgs = [m for m in body.get("messages") or [] if isinstance(m, dict)]
|
||||
letzte_antwort = max((i for i, m in enumerate(msgs) if m.get("role") == "assistant"), default=-1)
|
||||
frisch: list[tuple[dict, int]] = []
|
||||
alt: list[tuple[dict, int]] = []
|
||||
for i, m in enumerate(msgs):
|
||||
if not isinstance(m.get("content"), list):
|
||||
continue
|
||||
for j, part in enumerate(m["content"]):
|
||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||
(frisch if i > letzte_antwort else alt).append((m, j))
|
||||
return frisch, alt
|
||||
|
||||
|
||||
def bild_ziel(alias: str, vision_alias: str, coder_vision_alias: str | None) -> str:
|
||||
"""Bild-Zwilling für eine Anfrage mit frischem Bild: Coder-Ziele an den Coder-Zwilling (falls es ihn
|
||||
gibt), alles andere an den Hirn-Zwilling (vision)."""
|
||||
if coder_vision_alias and ist_coder_alias(alias):
|
||||
return coder_vision_alias
|
||||
return vision_alias
|
||||
|
||||
|
||||
def has_image(body: dict) -> bool:
|
||||
"""True, wenn irgendeine Nachricht einen Bild-Part enthaelt (OpenAI-multimodaler
|
||||
content: eine Liste mit einem {"type": "image_url"|"input_image"|"image", ...}-Teil)."""
|
||||
|
||||
+135
-131
@@ -1,131 +1,135 @@
|
||||
"""
|
||||
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"),
|
||||
"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": "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") 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"])
|
||||
|
||||
|
||||
def save_policy(patch: dict) -> dict:
|
||||
"""Validiert + persistiert atomar. Gibt die neue, vollständige Policy zurück."""
|
||||
clean = _coerce(patch) # wirft bei ungültigem Input
|
||||
with _LOCK:
|
||||
current = {**DEFAULTS, **_coerce_safe(_read_file()), **clean}
|
||||
POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = POLICY_PATH.with_suffix(".json.tmp")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(current, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, POLICY_PATH)
|
||||
_CACHE["mtime"] = None # nächster load_policy() lädt frisch
|
||||
_CACHE["policy"] = None
|
||||
return current
|
||||
|
||||
|
||||
def policy_meta() -> dict:
|
||||
"""Für den UI-Editor: aktuelle Werte + Defaults (für „Zurücksetzen“) + Feld-Spezifikation."""
|
||||
return {"policy": load_policy(), "defaults": dict(DEFAULTS), "fields": FIELDS}
|
||||
"""
|
||||
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"])
|
||||
|
||||
|
||||
def save_policy(patch: dict) -> dict:
|
||||
"""Validiert + persistiert atomar. Gibt die neue, vollständige Policy zurück."""
|
||||
clean = _coerce(patch) # wirft bei ungültigem Input
|
||||
with _LOCK:
|
||||
current = {**DEFAULTS, **_coerce_safe(_read_file()), **clean}
|
||||
POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = POLICY_PATH.with_suffix(".json.tmp")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(current, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, POLICY_PATH)
|
||||
_CACHE["mtime"] = None # nächster load_policy() lädt frisch
|
||||
_CACHE["policy"] = None
|
||||
return current
|
||||
|
||||
|
||||
def policy_meta() -> dict:
|
||||
"""Für den UI-Editor: aktuelle Werte + Defaults (für „Zurücksetzen“) + Feld-Spezifikation."""
|
||||
return {"policy": load_policy(), "defaults": dict(DEFAULTS), "fields": FIELDS}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Bild-Weiche v3 (24.09.2026): frische Bilder an den Bild-Zwilling, ältere als Beschreibung."""
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
from typing import ClassVar
|
||||
|
||||
from routers import gateway_proxy
|
||||
from services.router_logic import bild_ziel, bilder_aufteilen, ist_coder_alias
|
||||
|
||||
BILD_A = {"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}}
|
||||
BILD_B = {"type": "image_url", "image_url": {"url": "data:image/png;base64,BBBB"}}
|
||||
|
||||
|
||||
def verlauf() -> dict:
|
||||
"""Agenten-Ablauf: Bild A im ersten Schritt, danach Werkzeug-Aufruf, jetzt Bild B als Ergebnis."""
|
||||
return {"model": "coder", "messages": [
|
||||
{"role": "system", "content": "Du bist ein Coding-Agent."},
|
||||
{"role": "user", "content": [BILD_A, {"type": "text", "text": "Was zeigt der Fehler?"}]},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"id": "c1", "type": "function", "function": {"name": "screenshot", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "c1", "content": "Bildschirmfoto folgt."},
|
||||
{"role": "user", "content": [BILD_B, {"type": "text", "text": "Bildschirmfoto aus screenshot."}]},
|
||||
]}
|
||||
|
||||
|
||||
def test_frisch_ist_was_nach_der_letzten_antwort_kommt():
|
||||
frisch, alt = bilder_aufteilen(verlauf())
|
||||
assert [m["content"][i] for m, i in frisch] == [BILD_B]
|
||||
assert [m["content"][i] for m, i in alt] == [BILD_A]
|
||||
|
||||
|
||||
def test_ohne_antwort_sind_alle_bilder_frisch():
|
||||
frisch, alt = bilder_aufteilen({"messages": [{"role": "user", "content": [BILD_A, BILD_B]}]})
|
||||
assert len(frisch) == 2 and alt == []
|
||||
|
||||
|
||||
def test_coder_ziele_gehen_an_den_coder_zwilling_alles_andere_an_vision():
|
||||
assert ist_coder_alias("coder") and ist_coder_alias("heavy") and not ist_coder_alias("fast")
|
||||
assert bild_ziel("coder", "vision", "coder-bild") == "coder-bild"
|
||||
assert bild_ziel("heavy", "vision", "coder-bild") == "coder-bild"
|
||||
assert bild_ziel("fast", "vision", "coder-bild") == "vision"
|
||||
assert bild_ziel("coder", "vision", None) == "vision" # ohne Coder-Zwilling: Hirn-Zwilling
|
||||
|
||||
|
||||
class _FalscherClient:
|
||||
"""Ersetzt httpx.AsyncClient: zählt Beschreibungs-Anfragen und antwortet mit fester Beschreibung."""
|
||||
anfragen: ClassVar[list] = []
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def post(self, url, json):
|
||||
_FalscherClient.anfragen.append(json)
|
||||
|
||||
class Antwort:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
@staticmethod
|
||||
def json():
|
||||
return {"choices": [{"message": {"content": "Fehlermeldung: PORT 4711 belegt"}}]}
|
||||
return Antwort()
|
||||
|
||||
|
||||
def test_aeltere_bilder_werden_einmal_beschrieben_und_gemerkt(monkeypatch):
|
||||
monkeypatch.setattr(gateway_proxy.httpx, "AsyncClient", _FalscherClient)
|
||||
monkeypatch.setattr(gateway_proxy, "_BESCHREIBUNGEN", gateway_proxy.OrderedDict())
|
||||
_FalscherClient.anfragen = []
|
||||
|
||||
body = verlauf()
|
||||
_, alt = bilder_aufteilen(body)
|
||||
assert asyncio.run(gateway_proxy._alte_bilder_beschreiben(alt, "vision")) is True
|
||||
ersetzt = body["messages"][1]["content"][0]
|
||||
assert ersetzt["type"] == "text" and "PORT 4711" in ersetzt["text"]
|
||||
assert body["messages"][4]["content"][0] == BILD_B # das frische Bild bleibt ein Bild
|
||||
anfrage = _FalscherClient.anfragen[0]
|
||||
assert anfrage["model"] == "vision" and anfrage["chat_template_kwargs"] == {"enable_thinking": False}
|
||||
|
||||
# Nächster Schritt schickt denselben Verlauf erneut: aus dem Gedächtnis, keine zweite Anfrage.
|
||||
nochmal = verlauf()
|
||||
_, alt = bilder_aufteilen(nochmal)
|
||||
assert asyncio.run(gateway_proxy._alte_bilder_beschreiben(alt, "vision")) is True
|
||||
assert len(_FalscherClient.anfragen) == 1
|
||||
|
||||
|
||||
def test_zu_viele_neue_bilder_bleiben_drin(monkeypatch):
|
||||
monkeypatch.setattr(gateway_proxy, "_BESCHREIBUNGEN", gateway_proxy.OrderedDict())
|
||||
monkeypatch.setattr(gateway_proxy, "_BILD_MAX", 1)
|
||||
body = {"messages": [{"role": "user", "content": [BILD_A, BILD_B]}, {"role": "assistant", "content": "ok"},
|
||||
{"role": "user", "content": "und jetzt?"}]}
|
||||
vorher = copy.deepcopy(body)
|
||||
_, alt = bilder_aufteilen(body)
|
||||
assert asyncio.run(gateway_proxy._alte_bilder_beschreiben(alt, "vision")) is False
|
||||
assert body == vorher
|
||||
@@ -634,3 +634,23 @@ def test_baseline_misst_neu_wenn_sich_die_proben_geaendert_haben(ps, tmp_path, m
|
||||
def test_kein_ablenker_klingt_nach_plattenplatz(ps):
|
||||
namen = {w["function"]["name"] for w in ps.WERKZEUGE_HIRN}
|
||||
assert "system_speicher" not in namen and "system_arbeitsspeicher" in namen
|
||||
|
||||
|
||||
def test_baseline_misst_bilder_ueber_den_zwilling(monkeypatch, ps):
|
||||
"""Seit 24.09. sieht das heutige Hirn über seinen Bild-Zwilling (vision) — die Baseline muss das wissen."""
|
||||
hirn = {"name": "Qwen3.6-35B-A3B", "aliases": ["hermes", "fast"], "gguf_path": "/m/hirn.gguf",
|
||||
"cmd": "llama-server -m /m/hirn.gguf --spec-type draft-dflash"}
|
||||
zwilling = {"name": "Qwen3.6-35B-A3B-Bild", "aliases": ["vision"], "gguf_path": "/m/hirn.gguf",
|
||||
"cmd": "llama-server -m /m/hirn.gguf --mmproj /m/mmproj.gguf"}
|
||||
fremd = {"name": "Qwen3-VL", "aliases": [], "gguf_path": "/m/vl.gguf", "cmd": "llama-server -m /m/vl.gguf --mmproj x"}
|
||||
monkeypatch.setattr(radar.llamaswap, "list_models", lambda: [hirn, zwilling, fremd])
|
||||
aufrufe = []
|
||||
monkeypatch.setattr(ps, "baseline", lambda rolle, pfad, **kw: aufrufe.append(kw) or {"werte": {}})
|
||||
radar._baseline("hirn", time.time() + 60)
|
||||
assert aufrufe[0]["bild"] is True and aufrufe[0]["bild_modell"] == "vision"
|
||||
|
||||
# Ohne Zwilling (Projektor auf anderen Gewichten zählt nicht): keine Bild-Probe.
|
||||
monkeypatch.setattr(radar.llamaswap, "list_models", lambda: [hirn, fremd])
|
||||
radar._baseline("hirn", time.time() + 60)
|
||||
assert aufrufe[1]["bild"] is False and aufrufe[1]["bild_modell"] is None
|
||||
assert aufrufe[0]["kennung"] != aufrufe[1]["kennung"] # neuer Zwilling → neue Baseline
|
||||
|
||||
@@ -62,7 +62,7 @@ HERMES = os.path.expanduser("~/.local/bin/hermes")
|
||||
CTX_STANDARD = 65536
|
||||
# Stand der Proben. Ändern sich Aufgaben oder Werkzeuge, ist eine gecachte Baseline nicht mehr
|
||||
# vergleichbar — baseline() misst dann neu. Bei jeder Änderung an den Proben hochzählen.
|
||||
PRUEFSTAND_VERSION = 2
|
||||
PRUEFSTAND_VERSION = 3 # 24.09.: Coder darf vor dem Ändern lesen (Bild im Ablauf)
|
||||
# Flags wie am 17.09. (der Kontext kommt je Kandidat dazu): voller GPU-Offload, Flash-Attention,
|
||||
# kein mmap (--load-mode none, das alte --no-mmap gibt es seit b10936 nicht mehr), ein Slot,
|
||||
# KV-Cache q8_0 wie im Betrieb.
|
||||
@@ -709,6 +709,15 @@ def _bild_im_ablauf(basis: str, modell: str, rolle: str, url: str, frist: float
|
||||
]
|
||||
j = chat(basis, modell, verlauf, WERKZEUG_TOKENS, werkzeuge, frist=frist)
|
||||
aufrufe = _nachricht(j).get("tool_calls") or []
|
||||
lesen = next((a for a in aufrufe if (a.get("function") or {}).get("name") == "read"), None)
|
||||
if rolle != "hirn" and lesen and not trifft(aufrufe, erwartet):
|
||||
# Ein guter Coding-Agent liest die Datei, bevor er sie ändert (24.09.: der Coder tat genau das und
|
||||
# galt als gescheitert). Dann den Inhalt liefern und nach dem nächsten Schritt fragen.
|
||||
verlauf += [{"role": "assistant", "content": "", "tool_calls": [lesen]},
|
||||
{"role": "tool", "tool_call_id": lesen.get("id") or "call_lesen",
|
||||
"content": 'PORT = 8080\nHOST = "0.0.0.0"\n'}]
|
||||
j = chat(basis, modell, verlauf, WERKZEUG_TOKENS, werkzeuge, frist=frist)
|
||||
aufrufe = _nachricht(j).get("tool_calls") or []
|
||||
return {"ok": trifft(aufrufe, erwartet), "aufrufe": _aufrufe_kurz(aufrufe),
|
||||
"antwort": "" if aufrufe else _text(j)[:160]}
|
||||
|
||||
@@ -734,8 +743,10 @@ def messe_bild(basis: str, modell: str, rolle: str, frist: float | None = None)
|
||||
# --- Gesamtprüfung ---------------------------------------------------------------------
|
||||
|
||||
def pruefe(rolle: str, basis: str, modell: str, *, bild: bool = False, frist: float | None = None,
|
||||
protokoll=log) -> dict:
|
||||
"""Alle Proben der Rolle gegen ein laufendes Modell (Kandidat auf :5899 oder Baseline über llama-swap)."""
|
||||
protokoll=log, bild_modell: str | None = None) -> dict:
|
||||
"""Alle Proben der Rolle gegen ein laufendes Modell (Kandidat auf :5899 oder Baseline über llama-swap).
|
||||
bild_modell: wer die Bild-Probe macht, wenn es nicht das Modell selbst ist — seit 24.09. sehen Hirn und
|
||||
Coder über ihren Bild-Zwilling (vision, coder-bild), weil Draft und Bild in llama.cpp nicht zusammengehen."""
|
||||
if rolle not in ALIAS:
|
||||
raise ValueError(f"Unbekannte Rolle {rolle!r}")
|
||||
t0 = time.time()
|
||||
@@ -755,7 +766,7 @@ def pruefe(rolle: str, basis: str, modell: str, *, bild: bool = False, frist: fl
|
||||
werte["bild"] = None
|
||||
if bild:
|
||||
try:
|
||||
werte["bild"] = messe_bild(basis, modell, rolle, frist)
|
||||
werte["bild"] = messe_bild(basis, bild_modell or modell, rolle, frist)
|
||||
except Zeitende:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -948,7 +959,7 @@ def _schreibe_json(pfad: str, daten: dict) -> None:
|
||||
|
||||
def baseline(rolle: str, cache_pfad: str, *, kennung: str, bild: bool = False, frist: float | None = None,
|
||||
max_alter_tage: int = 30, basis_url: str = SWAP_URL, modell: str | None = None,
|
||||
protokoll=log) -> dict | None:
|
||||
protokoll=log, bild_modell: str | None = None) -> dict | None:
|
||||
"""Werte des heutigen Modells der Rolle, höchstens alle max_alter_tage neu gemessen.
|
||||
kennung = welches Modell mit welchen Flags (ändert sie sich, wird neu gemessen).
|
||||
Rückgabe {"kennung", "zeit", "werte"} oder None, wenn es weder Cache noch Messung gibt."""
|
||||
@@ -960,7 +971,8 @@ def baseline(rolle: str, cache_pfad: str, *, kennung: str, bild: bool = False, f
|
||||
return passend
|
||||
protokoll(f" Baseline {rolle} wird neu gemessen ({modell or ALIAS[rolle]} über llama-swap)")
|
||||
try:
|
||||
werte = pruefe(rolle, basis_url, modell or ALIAS[rolle], bild=bild, frist=frist, protokoll=protokoll)
|
||||
werte = pruefe(rolle, basis_url, modell or ALIAS[rolle], bild=bild, frist=frist, protokoll=protokoll,
|
||||
bild_modell=bild_modell)
|
||||
except Zeitende:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
@@ -27,6 +27,24 @@ models:
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 65536
|
||||
Qwen3.6-35B-A3B-Bild:
|
||||
# 24.09.2026: Bild-Zwilling des Hirns — dieselben Gewichte plus Bild-Projektor, aber OHNE Draft. llama.cpp
|
||||
# scheitert mit Draft UND Bild an „failed to process speculative batch“ (HTTP 500; b11057 und b11157 geprüft,
|
||||
# auch speculative.n_max=0 je Anfrage hilft nicht). Das MC2-Gateway schickt nur Anfragen mit einem Bild im
|
||||
# aktuellen Schritt hierher; ältere Bilder ersetzt es durch eine Beschreibung von hier, damit der Rest der
|
||||
# Aufgabe beim schnellen Hirn mit Draft bleibt. Probe 24.09.: 8/8 Bildmerkmale, Bildschirm-Aufgabe im
|
||||
# Agenten-Ablauf richtig, 67,9 t/s @13k (Hirn mit Draft 105,8). Projektor aus dem Original-Repo
|
||||
# (Qwen3.6-35B-A3B-MTP-GGUF) — er passt zur abliterierten Variante. Löst Qwen3-VL als `vision` ab.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/mmproj-BF16.gguf --jinja --parallel 1 -cram 8192 -ctk q8_0 -ctv q8_0 --reasoning-budget 2048
|
||||
ttl: 900
|
||||
aliases:
|
||||
- vision
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 65536
|
||||
Qwen3.8-27B:
|
||||
# Qwen 3.8 27B: Dichtes 27B-Modell mit hybrider Linear-Attention (48/64 Schichten linear),
|
||||
# nativem Multimodal-Support (mmproj-BF16) und Tool-Calling via --jinja.
|
||||
@@ -36,19 +54,35 @@ models:
|
||||
# 17.09.2026: Uncensored-Variante (JonathanColetti, Heretic-Abliteration: KL 0,12, Refusals 98->12/100, Benchmarks -0,5)
|
||||
# mit demselben DFlash2-Draft (Akzeptanz 0,82 @13k). Prüfstand: 30,4 t/s @13k (Original 26,2), Tools 6/6, Coding 3/3,
|
||||
# Hermes-Smoke grün. Original bleibt in Qwen3.8-27B-GGUF liegen — Rückweg = beide Pfade zurück.
|
||||
# 24.09.2026: ohne --mmproj. Bild UND DFlash2 ergaben immer HTTP 500; Bilder gehen jetzt an den Zwilling
|
||||
# Qwen3.8-27B-Bild (Alias coder-bild), der Text bleibt hier mit Draft schnell.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.8-27B-Uncensored-GGUF/mmproj-Qwen3.8-27B-Uncensored-F16.gguf --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf
|
||||
llama-server -m /srv/models/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf
|
||||
ttl: 5400
|
||||
aliases:
|
||||
- coder
|
||||
- heavy
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
in: [text]
|
||||
out: [text]
|
||||
tools: true
|
||||
# --parallel 1 = der Coder bekommt den ganzen Slot (34a9862, am laufenden Prozess
|
||||
# gegengeprueft). Er hat nur einen Verbraucher; Lucy/explore nutzen hermes, review heavy.
|
||||
context: 131072
|
||||
Qwen3.8-27B-Bild:
|
||||
# 24.09.2026: Bild-Zwilling des Coders (dieselben Gewichte plus Projektor, ohne DFlash2 — Grund siehe
|
||||
# Qwen3.6-35B-A3B-Bild). Nur für Anfragen mit einem Bild im aktuellen Schritt (Screenshot der App o. Ä.).
|
||||
# Probe 24.09.: 8/8 Bildmerkmale, 12,5 t/s @13k (Coder mit Draft 30,4).
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.8-27B-Uncensored-GGUF/mmproj-Qwen3.8-27B-Uncensored-F16.gguf --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0
|
||||
ttl: 900
|
||||
aliases:
|
||||
- coder-bild
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 131072
|
||||
Muse-Glimmer-30B:
|
||||
# Meta Muse Glimmer 30B: Dichtes 30B-Agenten-Modell mit DFlash-Speculative-Drafting
|
||||
# und 1.8B Perception Multimodal Projector. Ideal als Runtime-Debugger & Fehler-Diagnostiker.
|
||||
@@ -85,8 +119,7 @@ models:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/mmproj-F16.gguf --jinja
|
||||
ttl: 900
|
||||
aliases:
|
||||
- vision
|
||||
# 24.09.2026: `vision` an den Hirn-Zwilling abgegeben (Qwen3.6-35B-A3B-Bild); ohne Rolle, wird gelöscht.
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
@@ -111,3 +144,14 @@ groups:
|
||||
- Qwen3-Embedding-0.6B
|
||||
- Qwen3-Reranker-0.6B
|
||||
- Qwen3.6-35B-A3B
|
||||
bild:
|
||||
# 24.09.2026: die Bild-Zwillinge von Hirn und Coder. swap: true = höchstens einer von beiden geladen;
|
||||
# exclusive: false = das Laden verdrängt den Coder NICHT (sonst kostete jedes Bild in einer OpenChamber-
|
||||
# Sitzung den Coder samt Prompt-Cache). Speicher im schlimmsten Fall: Warm-Set 27 + Coder 29 +
|
||||
# Zwilling ~25 GB = ~81 GB. Nachts entlädt das Modell-Radar alles außerhalb des Warm-Sets.
|
||||
swap: true
|
||||
exclusive: false
|
||||
persistent: false
|
||||
members:
|
||||
- Qwen3.6-35B-A3B-Bild
|
||||
- Qwen3.8-27B-Bild
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as e,f as t,i as n,t as r,u as i}from"./button-Dd8hfusv.js";import{A as a,L as o,f as s,j as c,y as l}from"./index-B-ORi4lf.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(i(),1),g=e();function _({offen:e,onSchliessen:t}){let i=o(),_=s(),[v,y]=(0,h.useState)(null),[b,x]=(0,h.useState)(null),S=l(v);async function C(e){x(null);try{let t=await a(`/api/maintenance/restart`,{service:e});t.ok?c(`erfolg`,`${e} startet neu.`):c(`fehler`,t.err||`${e} ließ sich nicht neu starten.`),i.invalidateQueries({queryKey:[`dienste`]})}catch(e){c(`fehler`,e.message)}}return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel sm:max-w-2xl`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),(0,g.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-2`,children:(_.data?.services??[]).map(e=>(0,g.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,g.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,g.jsx)(`span`,{className:n(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:`bg-rot`),"aria-hidden":!0}),(0,g.jsxs)(`span`,{className:`min-w-0`,children:[(0,g.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,g.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:`antwortet nicht`]})]})]}),(0,g.jsxs)(`span`,{className:`flex gap-2`,children:[(0,g.jsx)(r,{variant:v===e.unit?`info`:`ghost`,size:`sm`,onClick:()=>y(e.unit),children:`Protokoll`}),b===e.unit?(0,g.jsx)(r,{variant:`gefahr`,size:`sm`,onClick:()=>C(e.unit),children:`Wirklich neu starten?`}):(0,g.jsx)(r,{variant:`outline`,size:`sm`,onClick:()=>x(e.unit),children:`Neu starten`})]})]},e.unit))}),v&&(0,g.jsx)(`pre`,{className:`ziffern mx-4 mb-4 max-h-[50vh] overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:S.isFetching?`Wird gelesen …`:S.data?.text||S.data?.err||`Kein Protokoll.`})]})})}export{_ as Dienste};
|
||||
import{c as e,f as t,i as n,t as r,u as i}from"./button-Dd8hfusv.js";import{A as a,L as o,f as s,j as c,y as l}from"./index-BRMEvo2A.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(i(),1),g=e();function _({offen:e,onSchliessen:t}){let i=o(),_=s(),[v,y]=(0,h.useState)(null),[b,x]=(0,h.useState)(null),S=l(v);async function C(e){x(null);try{let t=await a(`/api/maintenance/restart`,{service:e});t.ok?c(`erfolg`,`${e} startet neu.`):c(`fehler`,t.err||`${e} ließ sich nicht neu starten.`),i.invalidateQueries({queryKey:[`dienste`]})}catch(e){c(`fehler`,e.message)}}return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel sm:max-w-2xl`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),(0,g.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-2`,children:(_.data?.services??[]).map(e=>(0,g.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,g.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,g.jsx)(`span`,{className:n(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:`bg-rot`),"aria-hidden":!0}),(0,g.jsxs)(`span`,{className:`min-w-0`,children:[(0,g.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,g.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:`antwortet nicht`]})]})]}),(0,g.jsxs)(`span`,{className:`flex gap-2`,children:[(0,g.jsx)(r,{variant:v===e.unit?`info`:`ghost`,size:`sm`,onClick:()=>y(e.unit),children:`Protokoll`}),b===e.unit?(0,g.jsx)(r,{variant:`gefahr`,size:`sm`,onClick:()=>C(e.unit),children:`Wirklich neu starten?`}):(0,g.jsx)(r,{variant:`outline`,size:`sm`,onClick:()=>x(e.unit),children:`Neu starten`})]})]},e.unit))}),v&&(0,g.jsx)(`pre`,{className:`ziffern mx-4 mb-4 max-h-[50vh] overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:S.isFetching?`Wird gelesen …`:S.data?.text||S.data?.err||`Kein Protokoll.`})]})})}export{_ as Dienste};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{c as e,f as t,t as n,u as r}from"./button-Dd8hfusv.js";import{A as i,F as a,I as o,L as s,j as c,k as l}from"./index-B-ORi4lf.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=s(),_=o({queryKey:[`geheimnisse`],queryFn:()=>l(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``);async function b(e){try{await i(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),c(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),r.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){c(`fehler`,e.message)}}let x=_.data;return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel sm:max-w-lg`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Einstellungen`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Was die Box sich merkt.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-3 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hugging-Face-Zugang`}),(0,g.jsxs)(`p`,{className:`text-sm text-text-2`,children:[x?.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`,` Nötig für gesperrte Modelle und schnellere Downloads.`,x?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&b(v.trim())},children:[(0,g.jsx)(`label`,{htmlFor:`hf-token`,className:`sr-only`,children:`Hugging-Face-Zugang`}),(0,g.jsx)(`input`,{id:`hf-token`,type:`password`,autoComplete:`off`,value:v,onChange:e=>y(e.target.value),placeholder:`hf_…`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),(0,g.jsx)(n,{type:`submit`,disabled:!v.trim()||x?.schreibbar===!1,children:`Speichern`})]}),x?.hf_token_gesetzt&&!x.hf_token_aus_env&&(0,g.jsx)(n,{variant:`ghost`,size:`sm`,className:`self-start`,onClick:()=>b(null),children:`Zugang löschen`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hinweise`}),(0,g.jsx)(`p`,{className:`text-sm text-text-2`,children:`Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und schreibt es in den Verlauf.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4 pb-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hermes`}),(0,g.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3`,children:[(0,g.jsx)(a,{className:`size-4`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),(0,g.jsx)(`p`,{className:`text-xs text-text-3`,children:`Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.`})]})]})})}export{_ as Einstellungen};
|
||||
import{c as e,f as t,t as n,u as r}from"./button-Dd8hfusv.js";import{A as i,F as a,I as o,L as s,j as c,k as l}from"./index-BRMEvo2A.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=s(),_=o({queryKey:[`geheimnisse`],queryFn:()=>l(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``);async function b(e){try{await i(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),c(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),r.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){c(`fehler`,e.message)}}let x=_.data;return(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`w-full gap-3 overflow-y-auto border-linie bg-panel sm:max-w-lg`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Einstellungen`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Was die Box sich merkt.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-3 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hugging-Face-Zugang`}),(0,g.jsxs)(`p`,{className:`text-sm text-text-2`,children:[x?.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`,` Nötig für gesperrte Modelle und schnellere Downloads.`,x?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&b(v.trim())},children:[(0,g.jsx)(`label`,{htmlFor:`hf-token`,className:`sr-only`,children:`Hugging-Face-Zugang`}),(0,g.jsx)(`input`,{id:`hf-token`,type:`password`,autoComplete:`off`,value:v,onChange:e=>y(e.target.value),placeholder:`hf_…`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),(0,g.jsx)(n,{type:`submit`,disabled:!v.trim()||x?.schreibbar===!1,children:`Speichern`})]}),x?.hf_token_gesetzt&&!x.hf_token_aus_env&&(0,g.jsx)(n,{variant:`ghost`,size:`sm`,className:`self-start`,onClick:()=>b(null),children:`Zugang löschen`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hinweise`}),(0,g.jsx)(`p`,{className:`text-sm text-text-2`,children:`Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und schreibt es in den Verlauf.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4 pb-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hermes`}),(0,g.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3`,children:[(0,g.jsx)(a,{className:`size-4`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),(0,g.jsx)(`p`,{className:`text-xs text-text-3`,children:`Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.`})]})]})})}export{_ as Einstellungen};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{c as e,s as t}from"./button-Dd8hfusv.js";import{F as n,N as r,P as i}from"./index-B-ORi4lf.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-DWG2Q04V.js";var u=t(),d=e();function f(e){let t=(0,u.c)(17),{verbindung:f,onDienste:p,onEinstellungen:m,onSchliessen:h}=e,g;t[0]===h?g=t[1]:(g=e=>!e&&h(),t[0]=h,t[1]=g);let _;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,d.jsxs)(o,{children:[(0,d.jsx)(a,{className:`schild text-lg`,children:`Mehr`}),(0,d.jsx)(c,{className:`sr-only`,children:`Weitere Bereiche`})]}),t[2]=_):_=t[2];let v;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,d.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-base`,children:[(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=v):v=t[3];let y;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(y=(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),t[4]=y):y=t[4];let b;t[5]===p?b=t[6]:(b=(0,d.jsxs)(`button`,{type:`button`,onClick:p,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[y,` Dienste und Protokolle`]}),t[5]=p,t[6]=b);let x;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(x=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[7]=x):x=t[7];let S;t[8]===m?S=t[9]:(S=(0,d.jsxs)(`button`,{type:`button`,onClick:m,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[x,` Einstellungen`]}),t[8]=m,t[9]=S);let C;t[10]!==b||t[11]!==S||t[12]!==f?(C=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[_,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[f,v,b,S]})]}),t[10]=b,t[11]=S,t[12]=f,t[13]=C):C=t[13];let w;return t[14]!==g||t[15]!==C?(w=(0,d.jsx)(l,{open:!0,onOpenChange:g,children:C}),t[14]=g,t[15]=C,t[16]=w):w=t[16],w}export{f as MehrMenue};
|
||||
import{c as e,s as t}from"./button-Dd8hfusv.js";import{F as n,N as r,P as i}from"./index-BRMEvo2A.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-DWG2Q04V.js";var u=t(),d=e();function f(e){let t=(0,u.c)(17),{verbindung:f,onDienste:p,onEinstellungen:m,onSchliessen:h}=e,g;t[0]===h?g=t[1]:(g=e=>!e&&h(),t[0]=h,t[1]=g);let _;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,d.jsxs)(o,{children:[(0,d.jsx)(a,{className:`schild text-lg`,children:`Mehr`}),(0,d.jsx)(c,{className:`sr-only`,children:`Weitere Bereiche`})]}),t[2]=_):_=t[2];let v;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,d.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-base`,children:[(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=v):v=t[3];let y;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(y=(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),t[4]=y):y=t[4];let b;t[5]===p?b=t[6]:(b=(0,d.jsxs)(`button`,{type:`button`,onClick:p,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[y,` Dienste und Protokolle`]}),t[5]=p,t[6]=b);let x;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(x=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[7]=x):x=t[7];let S;t[8]===m?S=t[9]:(S=(0,d.jsxs)(`button`,{type:`button`,onClick:m,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[x,` Einstellungen`]}),t[8]=m,t[9]=S);let C;t[10]!==b||t[11]!==S||t[12]!==f?(C=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[_,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[f,v,b,S]})]}),t[10]=b,t[11]=S,t[12]=f,t[13]=C):C=t[13];let w;return t[14]!==g||t[15]!==C?(w=(0,d.jsx)(l,{open:!0,onOpenChange:g,children:C}),t[14]=g,t[15]=C,t[16]=w):w=t[16],w}export{f as MehrMenue};
|
||||
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>MC2 · Box-Wart</title>
|
||||
<script type="module" crossorigin src="/assets/index-B-ORi4lf.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BRMEvo2A.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/button-Dd8hfusv.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C3amg33Y.css">
|
||||
</head>
|
||||
|
||||
@@ -52,7 +52,13 @@ function Wert({ name, wert }: { name: string; wert: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Rolle({ titel, modell, laeuft, nutzer }: { titel: string; modell?: Modell; laeuft: boolean; nutzer: string }) {
|
||||
function Rolle({ titel, modell, zwilling, laeuft, nutzer }: {
|
||||
titel: string
|
||||
modell?: Modell
|
||||
zwilling?: Modell
|
||||
laeuft: boolean
|
||||
nutzer: string
|
||||
}) {
|
||||
const laden = useModellLaden()
|
||||
if (!modell) {
|
||||
return (
|
||||
@@ -64,7 +70,8 @@ function Rolle({ titel, modell, laeuft, nutzer }: { titel: string; modell?: Mode
|
||||
)
|
||||
}
|
||||
const kontext = modell.ctx ? Math.round(modell.ctx / Math.max(1, modell.parallel_slots) / 1024) : null
|
||||
const bilder = Boolean(modell.capabilities?.vision)
|
||||
// Seit 24.09.: Hirn und Coder sehen über ihren Bild-Zwilling (gleiche Gewichte, ohne Draft).
|
||||
const bilder = Boolean(modell.capabilities?.vision) || Boolean(zwilling)
|
||||
return (
|
||||
<article className="flex flex-col gap-3.5 rounded-2xl border border-linie bg-panel p-5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@@ -77,7 +84,10 @@ function Rolle({ titel, modell, laeuft, nutzer }: { titel: string; modell?: Mode
|
||||
<Wert name="Kontext" wert={kontext ? `${kontext}k` : "–"} />
|
||||
<Wert name="Bilder" wert={bilder ? "ja" : "nein"} />
|
||||
</dl>
|
||||
<p className="text-sm text-text-2">Genutzt von {nutzer}.{modell.spec_active ? " Mit Draft-Beschleunigung." : ""}</p>
|
||||
<p className="text-sm text-text-2">
|
||||
Genutzt von {nutzer}.{modell.spec_active ? " Mit Draft-Beschleunigung." : ""}
|
||||
{zwilling ? " Bilder sieht es über seinen Bild-Zwilling, der nur bei Bedarf lädt." : ""}
|
||||
</p>
|
||||
{!laeuft && (
|
||||
<Button variant="outline" size="sm" className="mt-auto self-start" onClick={() => laden.mutate(modell.name)} disabled={laden.isPending}>
|
||||
Jetzt laden
|
||||
@@ -275,15 +285,20 @@ export function ModelleSeite() {
|
||||
const laufend = modelle.data.running
|
||||
const hirn = liste.find((m) => rolleVon(m) === "hirn")
|
||||
const coder = liste.find((m) => rolleVon(m) === "coder")
|
||||
const weitere = liste.filter((m) => m !== hirn && m !== coder)
|
||||
// Bild-Zwilling: gleiche Gewichte, eigener Alias (vision bzw. coder-bild).
|
||||
const zwilling = (rolle: Modell | undefined, alias: string) =>
|
||||
rolle ? liste.find((m) => m !== rolle && m.aliases.includes(alias) && m.filename === rolle.filename) : undefined
|
||||
const hirnZwilling = zwilling(hirn, "vision")
|
||||
const coderZwilling = zwilling(coder, "coder-bild")
|
||||
const weitere = liste.filter((m) => ![hirn, coder, hirnZwilling, coderZwilling].includes(m))
|
||||
const zuletzt = nutzung.data?.zuletzt_geladen ?? {}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Speicher modelle={liste} laufend={laufend} />
|
||||
<section aria-label="Rollen" className="grid gap-5 md:grid-cols-3 md:gap-6">
|
||||
<Rolle titel="Hirn" modell={hirn} laeuft={!!hirn && laufend.includes(hirn.name)} nutzer="Lucy, NerdQuiz und OpenChamber für Kleinkram" />
|
||||
<Rolle titel="Coder" modell={coder} laeuft={!!coder && laufend.includes(coder.name)} nutzer="OpenChamber zum Planen und Bauen und Lucys Delegation" />
|
||||
<Rolle titel="Hirn" modell={hirn} zwilling={hirnZwilling} laeuft={!!hirn && laufend.includes(hirn.name)} nutzer="Lucy, NerdQuiz und OpenChamber für Kleinkram" />
|
||||
<Rolle titel="Coder" modell={coder} zwilling={coderZwilling} laeuft={!!coder && laufend.includes(coder.name)} nutzer="OpenChamber zum Planen und Bauen und Lucys Delegation" />
|
||||
<Rolle titel="Dritte Rolle" laeuft={false} nutzer="" />
|
||||
</section>
|
||||
<div className="grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)]">
|
||||
|
||||
Reference in New Issue
Block a user