feat(admin): Modell-Browser (Cloud) mit Tool-Badge, Preisen & $/Antwort
Phase 1 (Cloud) des Modell-Auswahlmenues: - Token je Turn im CostMeter (record_openrouter_cost liest prompt/completion_ tokens) -> globales Turn-Profil (turn_profile, EMA) fuer die $/Antwort-Schaetzung. - app/model_menu.py: gecachter OpenRouter-Katalog -> Chat-Modelle mit tool_supported (supported_parameters), eval_verified (kuratiert), Up/Down-Preisen pro Mio. Token und geschaetzter $/Antwort (Profil x Modellpreise). - GET /api/admin/model-menu; durchsuchbarer Browser im System-Tab, "Verwenden" setzt openrouter_llm_model (Freitext/Preset bleiben). Lokale Modelle (Durchsatz/Strom): Phase 2. Konzept in Ideen/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
94740daa77
commit
a87dc75a46
5 changed files with 233 additions and 3 deletions
34
app/store.py
34
app/store.py
|
|
@ -136,6 +136,14 @@ class Store(ABC):
|
|||
def cost_for_day(self, user_id: str, day: str | None = None) -> float:
|
||||
"""Summe der Kosten (USD) des Nutzers am Tag (Default: heute, UTC)."""
|
||||
|
||||
@abstractmethod
|
||||
def update_turn_profile(self, prompt_tokens: int, completion_tokens: int) -> None:
|
||||
"""Aktualisiert das gleitende Mittel der Token je Turn (für „$/Antwort")."""
|
||||
|
||||
@abstractmethod
|
||||
def get_turn_profile(self) -> dict:
|
||||
"""Liefert {avg_prompt, avg_completion, samples} oder {} (noch ungemessen)."""
|
||||
|
||||
@abstractmethod
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
"""Loescht einen Nutzer und alle seine Daten (Sessions, Nachrichten, Erinnerungen,
|
||||
|
|
@ -272,6 +280,10 @@ class SQLiteStore(Store):
|
|||
amount_usd REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, day, category)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS turn_profile (
|
||||
k TEXT PRIMARY KEY,
|
||||
v REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS emergency_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
|
|
@ -569,6 +581,28 @@ class SQLiteStore(Store):
|
|||
).fetchone()
|
||||
return float(row["s"]) if row else 0.0
|
||||
|
||||
def update_turn_profile(self, prompt_tokens: int, completion_tokens: int,
|
||||
alpha: float = 0.1) -> None:
|
||||
with self._connect() as conn:
|
||||
cur = {r["k"]: r["v"] for r in conn.execute("SELECT k, v FROM turn_profile")}
|
||||
n = cur.get("samples", 0)
|
||||
if n <= 0:
|
||||
ap, ac = float(prompt_tokens), float(completion_tokens)
|
||||
else:
|
||||
ap = (1 - alpha) * cur.get("avg_prompt", 0.0) + alpha * prompt_tokens
|
||||
ac = (1 - alpha) * cur.get("avg_completion", 0.0) + alpha * completion_tokens
|
||||
for k, val in (("avg_prompt", ap), ("avg_completion", ac), ("samples", n + 1)):
|
||||
conn.execute(
|
||||
"INSERT INTO turn_profile(k, v) VALUES(?, ?)"
|
||||
" ON CONFLICT(k) DO UPDATE SET v = excluded.v",
|
||||
(k, val),
|
||||
)
|
||||
|
||||
def get_turn_profile(self) -> dict:
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute("SELECT k, v FROM turn_profile").fetchall()
|
||||
return {r["k"]: r["v"] for r in rows}
|
||||
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
if user_id == ANONYMOUS_USER_ID:
|
||||
raise ValueError("Der anonyme Nutzer kann nicht geloescht werden.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue