Nach einem Sicherheitsfilter-Treffer fuhr ein Projekt still auf dem Ausweich-Modell weiter, und nirgends war zu sehen, auf welchem. Zurueck ging nur ueber "Liste leeren" im Filter-Panel, und das gleich fuer alle Chats. Jetzt drei Ebenen (aria-brain/context_model.py): Einstellung (brainModel) < pro Chat < Ausweich (Sicherheitsfilter) - Pro Chat: Hauptchat und jedes Projekt koennen ein eigenes Modell haben, in /shared/config/context_models.json. Bewusst kein Feld im Projekt-Manifest — der Hauptchat ist kein Projekt und waere sonst aussen vor. - "↩ zurueck" pro Chat: loescht nur die Filter-Vermerke DIESES Chats. Blockt der Filter erneut, weicht das Brain eben wieder aus. - Angezeigt wird, was wirklich laeuft: chat() gibt das Modell aus modelUsage der CLI zurueck (aufgeloeste Version statt Alias) samt Grund; Brain → Bridge → chat_backup + Live-Payload → App und Diagnostic. Als Grundeinstellung zaehlt das Modell, mit dem das Brain laeuft, nicht der Dateiwert — so faellt ein ausstehender Neustart nach brainModel-Aenderung auf. Diagnostic: 🧠-Auswahl neben "+ Projekt" fuer den fokussierten Chat, Modell im Untertitel jeder Kontext-Karte, Modell statt "Claude" am Bubble-Badge (orange mit ↪ bei Ausweich). App: hinter der Quell-Anzeige (Einstellungen, Default aus — Kopfzeile bleibt fuer Mama unveraendert) ein Modell-Chip in der Kopfzeile; Tippen oeffnet die Auswahl fuer diesen Chat inkl. "Zurueck". Laeuft ueber den vorhandenen brain_request-Kanal, also keine neuen RVS-Typen. Tests: 47/47 (12 neue fuer die Aufloesungsreihenfolge gegen agent.chat()), tsc ohne Fehler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
"""Welches Modell gilt in welchem Chat.
|
|
|
|
Drei Ebenen, von unten nach oben:
|
|
|
|
1. Einstellung — `brainModel` aus runtime.json (Diagnostic → Sprachmodell).
|
|
Gilt fuer alles, was keine eigene Wahl hat.
|
|
2. Pro Chat — ein Override je Kontext ("" = Hauptchat, sonst project_id).
|
|
Damit kann ein Projekt bewusst auf einem anderen Modell laufen, ohne das
|
|
Hauptmodell fuer alles umzustellen.
|
|
3. Ausweich — Anthropics Sicherheitsfilter hat das gewaehlte Modell an einem
|
|
Turn blockiert, der noch im Fenster liegt (safeguard.py). Das sticht alles,
|
|
ist aber nur so lange aktiv, wie der Turn im Fenster steht.
|
|
|
|
Warum ueberhaupt ein eigener Speicher und kein Feld im Projekt-Manifest: der
|
|
Hauptchat IST kein Projekt. Ein Manifest-Feld haette ihn aussen vor gelassen,
|
|
und genau dort faellt eine Modellwahl am ehesten an.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Tuple
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STORE_FILE = Path(os.environ.get("CONTEXT_MODELS_FILE",
|
|
"/shared/config/context_models.json"))
|
|
|
|
# Begruendungen, wie sie die Oberflaechen anzeigen. Bewusst kurze Schluessel —
|
|
# App und Diagnostic uebersetzen sie selbst, damit die Texte dort hingehoeren,
|
|
# wo sie gelesen werden.
|
|
REASON_SETTING = "einstellung"
|
|
REASON_PER_CHAT = "pro-chat"
|
|
REASON_FALLBACK = "ausweich"
|
|
|
|
_lock = threading.Lock()
|
|
|
|
|
|
def _load() -> dict:
|
|
try:
|
|
if STORE_FILE.exists():
|
|
data = json.loads(STORE_FILE.read_text(encoding="utf-8")) or {}
|
|
if isinstance(data, dict):
|
|
# Nur String→String; alles andere waere ein Schreibfehler von
|
|
# aussen und wuerde sonst still als Modellname durchgehen.
|
|
return {str(k): str(v) for k, v in data.items() if isinstance(v, str)}
|
|
except Exception as exc:
|
|
logger.warning("[context-model] lesen fehlgeschlagen: %s", exc)
|
|
return {}
|
|
|
|
|
|
def _save(data: dict) -> None:
|
|
try:
|
|
STORE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = STORE_FILE.with_suffix(".json.tmp")
|
|
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
tmp.replace(STORE_FILE)
|
|
except Exception as exc:
|
|
logger.warning("[context-model] schreiben fehlgeschlagen: %s", exc)
|
|
|
|
|
|
def all_overrides() -> dict:
|
|
return _load()
|
|
|
|
|
|
def get(context_id: str) -> str:
|
|
"""Der Override dieses Kontexts, oder "" wenn keiner gesetzt ist."""
|
|
return _load().get(context_id or "", "")
|
|
|
|
|
|
def set(context_id: str, model: str) -> str:
|
|
"""Override setzen. Leerer Modellname loescht ihn (= wieder Einstellung)."""
|
|
cid = context_id or ""
|
|
m = (model or "").strip()
|
|
with _lock:
|
|
data = _load()
|
|
if m:
|
|
data[cid] = m
|
|
else:
|
|
data.pop(cid, None)
|
|
_save(data)
|
|
logger.info("[context-model] %s → %s", cid or "(Hauptchat)", m or "(Einstellung)")
|
|
return m
|
|
|
|
|
|
def resolve(context_id: str, default_model: str) -> Tuple[str, str]:
|
|
"""(Modell, Begruendung) ohne Sicherheitsfilter — den kennt nur der Agent."""
|
|
ov = get(context_id)
|
|
if ov:
|
|
return ov, REASON_PER_CHAT
|
|
return default_model, REASON_SETTING
|