feat(costs): Pro-Nutzer-Kostenerfassung Stufe 1 (OpenRouter-Ist-Kosten)
CostMeter via ContextVars (app/core/costs.py, multitool-ready). Alle
OpenRouter-Calls (LLM, Sonar, Koreferenz, Fallback) senden usage:{include:true}
und buchen die Ist-Kosten je Kategorie (llm/web_search) — inkl. finalem
Streaming-usage-Chunk. cost_usage-Tabelle (user/day/category) + Store-Methoden;
ws.py/chat.py start_meter pro Turn + persist_costs. Statistik: Spalten
"Kosten (Monat)" + "Ø/Anfrage" + Gesamt. Live: 1 Such-Turn ~ $0,0052
(web_search dominiert, LLM ~ $0,00007). Tests: 318.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
21a5250282
commit
d87f0ce424
9 changed files with 171 additions and 10 deletions
47
app/store.py
47
app/store.py
|
|
@ -128,6 +128,14 @@ class Store(ABC):
|
|||
def add_web_search_usage(self, user_id: str, n: int = 1, day: str | None = None) -> int:
|
||||
"""Zaehlt n Web-Suchen dazu und liefert die neue Tages-Zahl."""
|
||||
|
||||
@abstractmethod
|
||||
def add_cost_usage(self, user_id: str, category: str, usd: float, day: str | None = None) -> None:
|
||||
"""Bucht Kosten (USD) auf Nutzer/Tag/Kategorie."""
|
||||
|
||||
@abstractmethod
|
||||
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 delete_user(self, user_id: str) -> bool:
|
||||
"""Loescht einen Nutzer und alle seine Daten (Sessions, Nachrichten, Erinnerungen,
|
||||
|
|
@ -243,6 +251,13 @@ class SQLiteStore(Store):
|
|||
count INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, day)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS cost_usage (
|
||||
user_id TEXT NOT NULL,
|
||||
day TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
amount_usd REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, day, category)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS emergency_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
|
|
@ -517,6 +532,29 @@ class SQLiteStore(Store):
|
|||
).fetchone()
|
||||
return int(row["count"])
|
||||
|
||||
def add_cost_usage(self, user_id: str, category: str, usd: float,
|
||||
day: str | None = None) -> None:
|
||||
if not usd or usd <= 0:
|
||||
return
|
||||
day = day or self._today()
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO cost_usage (user_id, day, category, amount_usd) VALUES (?, ?, ?, ?)"
|
||||
" ON CONFLICT(user_id, day, category) DO UPDATE SET"
|
||||
" amount_usd = amount_usd + excluded.amount_usd",
|
||||
(user_id, day, category, float(usd)),
|
||||
)
|
||||
|
||||
def cost_for_day(self, user_id: str, day: str | None = None) -> float:
|
||||
day = day or self._today()
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT COALESCE(SUM(amount_usd), 0) AS s FROM cost_usage"
|
||||
" WHERE user_id = ? AND day = ?",
|
||||
(user_id, day),
|
||||
).fetchone()
|
||||
return float(row["s"]) if row else 0.0
|
||||
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
if user_id == ANONYMOUS_USER_ID:
|
||||
raise ValueError("Der anonyme Nutzer kann nicht geloescht werden.")
|
||||
|
|
@ -532,6 +570,7 @@ class SQLiteStore(Store):
|
|||
conn.execute("DELETE FROM memories WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM usage WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM web_search_usage WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM cost_usage WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||
return True
|
||||
|
||||
|
|
@ -639,15 +678,21 @@ class SQLiteStore(Store):
|
|||
return [dict(r) for r in rows]
|
||||
|
||||
def get_all_usage(self) -> list[dict]:
|
||||
month = datetime.now(timezone.utc).strftime("%Y-%m") + "-%"
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT u.user_id, COALESCE(usr.display_name, u.user_id) AS display_name,"
|
||||
" SUM(u.requests) AS total_requests, SUM(u.units) AS total_units,"
|
||||
" MAX(u.day) AS last_active,"
|
||||
" COALESCE((SELECT SUM(w.count) FROM web_search_usage w"
|
||||
" WHERE w.user_id = u.user_id), 0) AS total_web_searches"
|
||||
" WHERE w.user_id = u.user_id), 0) AS total_web_searches,"
|
||||
" COALESCE((SELECT SUM(c.amount_usd) FROM cost_usage c"
|
||||
" WHERE c.user_id = u.user_id AND c.day LIKE ?), 0) AS month_cost_usd,"
|
||||
" COALESCE((SELECT SUM(uu.requests) FROM usage uu"
|
||||
" WHERE uu.user_id = u.user_id AND uu.day LIKE ?), 0) AS month_requests"
|
||||
" FROM usage u LEFT JOIN users usr ON usr.id = u.user_id"
|
||||
" GROUP BY u.user_id ORDER BY total_requests DESC",
|
||||
(month, month),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue