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

@ -11,6 +11,7 @@ from dataclasses import dataclass, field
import httpx
from app.core.costs import record_openrouter_cost
from app.metrics import metrics
from app.providers.llm.base import lang_instruction
@ -70,7 +71,8 @@ class SonarTool:
logger.warning("SonarTool ohne API-Key")
return ToolResult(text=_NO_DATA, ok=False)
payload = {"model": self.model, "messages": self._messages(query, language)}
payload = {"model": self.model, "messages": self._messages(query, language),
"usage": {"include": True}}
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
timeout = httpx.Timeout(self.timeout)
@ -90,6 +92,7 @@ class SonarTool:
return ToolResult(text=_NO_DATA, ok=False)
data = resp.json()
record_openrouter_cost(data.get("usage"), "web_search")
try:
content = data["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError):