feat(costs): Filler-Übersetzung + OpenRouter-TTS kostenerfasst

- fillers.py translate_pool: usage:{include:true} + Ist-Kosten (Kategorie llm).
- OpenRouter-TTS: geschaetzte Kosten (Zeichen × openrouter_tts_usd_per_char,
  Kategorie tts) — der Audio-Endpunkt liefert keine inline-usage. Preis (Default
  ~$15/1M Zeichen) live-setzbar; an Modellpreis anpassbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 19:25:34 +02:00
commit a1e49dde11
5 changed files with 23 additions and 2 deletions

View file

@ -194,6 +194,7 @@ class Settings(BaseSettings):
# Kostenerfassung Stufe 2 (geschätzte Quellen). Siehe Docs/kosten-erfassung.md.
usd_per_eur: float = 1.08 # Umrechnung EUR-naher Schätzungen in die USD-Anzeige
cartesia_usd_per_char: float = 0.00005 # Cartesia: 100k Credits ≈ 100.000 Zeichen → $5
openrouter_tts_usd_per_char: float = 0.000015 # OpenRouter-TTS (Schätzung, ≈ $15/1M Zeichen) — an Modellpreis anpassen
emergency_alert_eur: float = 0.20 # SMS+Voice pro Alarm (Schätzung)
# Lokales LLM: Stromkosten pro Anfrage (marginal über Leerlauf), 2× RTX 3090.
gpu_idle_watts: float = 40.0 # Leerlauf je GPU = Infrastruktur (nicht pro Anfrage)

View file

@ -102,6 +102,17 @@ def cartesia_usd(n_chars, cfg=None) -> float:
return max(0, n) * cfg.cartesia_usd_per_char
def openrouter_tts_usd(n_chars, cfg=None) -> float:
"""OpenRouter-TTS-Kosten (USD) fuer `n_chars` (Schaetzung — der Audio-Endpunkt
liefert keine inline-Kosten wie der Chat-Endpunkt)."""
cfg = _cfg(cfg)
try:
n = int(n_chars or 0)
except (TypeError, ValueError):
return 0.0
return max(0, n) * cfg.openrouter_tts_usd_per_char
def eur_to_usd(eur, cfg=None) -> float:
cfg = _cfg(cfg)
try:

View file

@ -17,6 +17,8 @@ import re
import httpx
from app.core.costs import record_openrouter_cost
logger = logging.getLogger(__name__)
ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
@ -107,13 +109,15 @@ def make_openrouter_translator(api_key: str, model: str):
f"no quotes, no extra words. Output ONLY a numbered list of the translations."
f"\n\n{numbered}"
)
payload = {"model": model, "temperature": 0.4,
payload = {"model": model, "temperature": 0.4, "usage": {"include": True},
"messages": [{"role": "user", "content": prompt}]}
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
resp = await client.post(ENDPOINT, json=payload, headers=headers)
resp.raise_for_status()
text = resp.json()["choices"][0]["message"]["content"]
data = resp.json()
record_openrouter_cost(data.get("usage"), "llm")
text = data["choices"][0]["message"]["content"]
return _parse_numbered(text, len(phrases))
return translate_pool

View file

@ -2,6 +2,7 @@ import asyncio
import httpx
from app.core.costs import add_cost, openrouter_tts_usd
from app.providers.tts.base import TTSProvider
# Preview-TTS-Modelle liefern gelegentlich HTTP 200 mit LEEREM Body oder ein
@ -70,6 +71,9 @@ class OpenRouterTTSProvider(TTSProvider):
raise RuntimeError(f"OpenRouter TTS transport error: {exc}") from exc
else:
if response.content:
# Geschätzte TTS-Kosten (Zeichen × Preis); Audio-Endpunkt
# liefert keine inline-usage.
add_cost(openrouter_tts_usd(len(text.strip())), "tts")
return response.content
# HTTP 200 mit leerem Body -> transienter Aussetzer, erneut versuchen.

View file

@ -39,6 +39,7 @@ RUNTIME_SETTABLE: dict[str, tuple[str, str, str]] = {
"electricity_price_eur_per_kwh": ("Strompreis (€/kWh)", "float", "Kosten lokaler LLM-Anfragen (z.B. 0.33)"),
"emergency_alert_eur": ("Notruf-Kosten (€/Alarm)", "float", "SMS+Anruf pro Alarm (z.B. 0.20)"),
"cartesia_usd_per_char": ("Cartesia-TTS ($/Zeichen)", "float", "z.B. 0.00005 (100k Credits ≈ 100.000 Zeichen)"),
"openrouter_tts_usd_per_char": ("OpenRouter-TTS ($/Zeichen)", "float", "Schätzung, z.B. 0.000015 — an Modellpreis anpassen"),
"emergency_cooldown_minutes": ("Notruf-Sperre (Minuten)", "int", "Erneuter Alarm für N Minuten blockiert (Standard 5, 0 = aus)"),
}