feat: Modell pro Chat waehlen — und sehen, welches gerade laeuft
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>
This commit is contained in:
+115
-1
@@ -39,6 +39,7 @@ import background as background_mod
|
||||
import oauth as oauth_mod
|
||||
import seed_rules as seed_rules_mod
|
||||
import safeguard as safeguard_mod
|
||||
import context_model as context_model_mod
|
||||
import projects as projects_mod
|
||||
import project_vms as project_vms_mod
|
||||
|
||||
@@ -680,6 +681,11 @@ class ChatOut(BaseModel):
|
||||
# Welcher Backend die Antwort erzeugt hat: "local" (Qwen), "claude",
|
||||
# "fast-path" (Skill/Regex). Fuer den Quell-Badge in Diagnostic.
|
||||
answered_by: str = "claude"
|
||||
# Welches Claude-Modell geantwortet hat (aus modelUsage der CLI, also die
|
||||
# aufgeloeste Version) und WARUM dieses: "einstellung" | "pro-chat" |
|
||||
# "ausweich" (Sicherheitsfilter). Leer, wenn kein Claude-Modell lief.
|
||||
model: str = ""
|
||||
model_reason: str = ""
|
||||
# Soll die Antwort vorgelesen werden? Fast-Path (reiner Steuerbefehl) = False;
|
||||
# ARIA-Antworten (local/claude) = True. System-Flag statt <voice>-Tag.
|
||||
speak: bool = True
|
||||
@@ -778,7 +784,7 @@ async def chat(body: ChatIn, background: BackgroundTasks):
|
||||
# Sync-Aufruf im Executor damit wir den Event-Loop nicht blocken —
|
||||
# chat() macht HTTP-Calls (Proxy) die 30-60s dauern koennen.
|
||||
loop = asyncio.get_running_loop()
|
||||
reply, answered_by, speak, converse, awaiting_reply = await loop.run_in_executor(
|
||||
reply, answered_by, speak, converse, awaiting_reply, model_info = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: a.chat(
|
||||
body.message, source=body.source, project_id=pid,
|
||||
@@ -801,6 +807,8 @@ async def chat(body: ChatIn, background: BackgroundTasks):
|
||||
events=a.pop_events(),
|
||||
project_id=pid,
|
||||
answered_by=answered_by,
|
||||
model=(model_info or {}).get("model", ""),
|
||||
model_reason=(model_info or {}).get("reason", ""),
|
||||
speak=speak,
|
||||
converse=converse,
|
||||
awaiting_reply=awaiting_reply,
|
||||
@@ -1265,6 +1273,112 @@ def safeguard_clear(body: SafeguardClearBody):
|
||||
return {"ok": True, "removed": removed}
|
||||
|
||||
|
||||
# ─── Modell pro Chat ────────────────────────────────────────────────
|
||||
# Einstellung (brainModel) < pro Chat < Ausweich (Sicherheitsfilter). Siehe
|
||||
# context_model.py. Diese Endpunkte liefern fuer jede Oberflaeche dieselbe
|
||||
# Sicht: was ist eingestellt, was laeuft WIRKLICH, und warum.
|
||||
|
||||
_MODEL_CHOICES_CACHE: dict = {"at": 0.0, "data": []}
|
||||
|
||||
|
||||
def _model_choices() -> list:
|
||||
"""Modell-Auswahl fuer die Pickers — dieselbe Liste wie in der Diagnostic
|
||||
(Proxy /v1/models, aus Anthropics Katalog). 10 Minuten im Speicher, denn
|
||||
die App fragt bei jedem Oeffnen des Pickers. Fail-soft: ohne Proxy eine
|
||||
leere Liste, die Anzeige des aktiven Modells funktioniert trotzdem."""
|
||||
import time as _t
|
||||
import httpx as _httpx
|
||||
now = _t.time()
|
||||
if _MODEL_CHOICES_CACHE["data"] and now - _MODEL_CHOICES_CACHE["at"] < 600:
|
||||
return _MODEL_CHOICES_CACHE["data"]
|
||||
try:
|
||||
base = os.environ.get("PROXY_URL", "http://proxy:3456").rstrip("/")
|
||||
r = _httpx.get(f"{base}/v1/models", timeout=8)
|
||||
data = [{
|
||||
"id": m.get("id", ""),
|
||||
"name": m.get("display_name") or m.get("id", ""),
|
||||
"section": m.get("section") or "tier",
|
||||
} for m in (r.json().get("data") or []) if m.get("id")]
|
||||
if data:
|
||||
_MODEL_CHOICES_CACHE.update(at=now, data=data)
|
||||
return data
|
||||
except Exception as exc:
|
||||
logger.warning("[context-model] Modell-Liste vom Proxy nicht ladbar: %s", exc)
|
||||
return _MODEL_CHOICES_CACHE["data"]
|
||||
|
||||
|
||||
def _context_model_rows() -> dict:
|
||||
a = agent()
|
||||
conv = conversation()
|
||||
# Das Modell, mit dem das Brain gerade wirklich laeuft — NICHT neu aus
|
||||
# runtime.json gelesen. brainModel greift erst nach Neustart; zeigten wir
|
||||
# hier den Dateiwert, saehe ein ausstehender Neustart aus wie erledigt.
|
||||
default = a.proxy.model
|
||||
contexts = [("", "Hauptchat")] + [
|
||||
(p["id"], p.get("name") or p["id"])
|
||||
for p in projects_mod.list_projects()
|
||||
]
|
||||
rows = []
|
||||
for cid, name in contexts:
|
||||
override = context_model_mod.get(cid)
|
||||
model, reason = context_model_mod.resolve(cid, default)
|
||||
window = safeguard_mod.filter_window(conv.window(project_id=cid))
|
||||
blocked = safeguard_mod.blocked_models(cid, [t.ts for t in window])
|
||||
fallback = ""
|
||||
if safeguard_mod.norm_model(model) in blocked:
|
||||
fallback = safeguard_mod.fallback_for(model)
|
||||
reason = context_model_mod.REASON_FALLBACK
|
||||
rows.append({
|
||||
"context_id": cid,
|
||||
"name": name,
|
||||
"override": override, # "" = folgt der Einstellung
|
||||
"selected": model, # was gewaehlt ist (Einstellung/pro Chat)
|
||||
"effective": fallback or model, # was der naechste Turn nutzt
|
||||
"reason": reason,
|
||||
"blocked": sorted(blocked),
|
||||
})
|
||||
choices = _model_choices()
|
||||
names = {c["id"]: c["name"] for c in choices}
|
||||
return {"default": default, "rows": rows, "choices": choices, "names": names}
|
||||
|
||||
|
||||
@app.get("/context-model/list")
|
||||
def context_model_list():
|
||||
return _context_model_rows()
|
||||
|
||||
|
||||
class ContextModelBody(BaseModel):
|
||||
context_id: str = ""
|
||||
model: str = "" # leer = Override loeschen, Einstellung gilt wieder
|
||||
|
||||
|
||||
@app.post("/context-model/set")
|
||||
def context_model_set(body: ContextModelBody):
|
||||
cid = (body.context_id or "").strip()
|
||||
if cid and not projects_mod.get_project(cid):
|
||||
raise HTTPException(404, f"Projekt {cid} nicht gefunden")
|
||||
context_model_mod.set(cid, body.model)
|
||||
return _context_model_rows()
|
||||
|
||||
|
||||
class ContextModelResetBody(BaseModel):
|
||||
context_id: str = ""
|
||||
|
||||
|
||||
@app.post("/context-model/back-to-selected")
|
||||
def context_model_back(body: ContextModelResetBody):
|
||||
"""Ausweich beenden: Sicherheitsfilter-Vermerke dieses Chats loeschen.
|
||||
|
||||
Danach laeuft der naechste Turn wieder auf dem gewaehlten Modell. Steht
|
||||
der ausloesende Text noch im Verlauf, kann der Filter erneut greifen —
|
||||
dann weicht das Brain eben wieder aus. Das ist gewollt: zurueck soll
|
||||
jederzeit gehen, ohne dass man vorher irgendwas aufraeumen muss."""
|
||||
removed = safeguard_mod.clear((body.context_id or "").strip())
|
||||
out = _context_model_rows()
|
||||
out["removed"] = removed
|
||||
return out
|
||||
|
||||
|
||||
# ─── Call-Metrics (Token / Quota-Monitoring) ────────────────────────
|
||||
|
||||
@app.get("/metrics/calls")
|
||||
|
||||
Reference in New Issue
Block a user