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:
Dieter Schlüter 2026-06-30 11:08:45 +02:00
commit d87f0ce424
9 changed files with 171 additions and 10 deletions

View file

@ -9,6 +9,8 @@ import logging
import httpx
from app.core.costs import record_openrouter_cost
logger = logging.getLogger(__name__)
ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
@ -66,7 +68,7 @@ class Decontextualizer:
return text
user = (f"Conversation:\n{self._render(history)}\n\n"
f"Latest message: {text}\n\nRewritten self-contained message:")
payload = {"model": self.model, "temperature": 0.0,
payload = {"model": self.model, "temperature": 0.0, "usage": {"include": True},
"messages": [{"role": "system", "content": _SYSTEM},
{"role": "user", "content": user}]}
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
@ -74,7 +76,9 @@ class Decontextualizer:
async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client:
resp = await client.post(ENDPOINT, json=payload, headers=headers)
resp.raise_for_status()
out = (resp.json()["choices"][0]["message"]["content"] or "").strip()
data = resp.json()
record_openrouter_cost(data.get("usage"), "llm")
out = (data["choices"][0]["message"]["content"] or "").strip()
except Exception as exc: # noqa: BLE001 — best effort, bei Fehler Original behalten
logger.warning("Decontextualizer fehlgeschlagen: %s", exc)
return text