From a1e49dde112b61ba0c8ef80e598b70cadbf7e926 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 30 Jun 2026 19:25:34 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(costs):=20Filler-=C3=9Cbersetzung=20+?= =?UTF-8?q?=20OpenRouter-TTS=20kostenerfasst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/config.py | 1 + app/core/costs.py | 11 +++++++++++ app/pipeline/fillers.py | 8 ++++++-- app/providers/tts/openrouter.py | 4 ++++ app/runtime_config.py | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/config.py b/app/config.py index 3abea11..9c74667 100644 --- a/app/config.py +++ b/app/config.py @@ -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) diff --git a/app/core/costs.py b/app/core/costs.py index 350d1ba..0d0ade0 100644 --- a/app/core/costs.py +++ b/app/core/costs.py @@ -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: diff --git a/app/pipeline/fillers.py b/app/pipeline/fillers.py index 9ccbe0b..c7a246a 100644 --- a/app/pipeline/fillers.py +++ b/app/pipeline/fillers.py @@ -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 diff --git a/app/providers/tts/openrouter.py b/app/providers/tts/openrouter.py index dda8e5f..5ffa6a0 100644 --- a/app/providers/tts/openrouter.py +++ b/app/providers/tts/openrouter.py @@ -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. diff --git a/app/runtime_config.py b/app/runtime_config.py index 60c66b1..7229f40 100644 --- a/app/runtime_config.py +++ b/app/runtime_config.py @@ -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)"), } From 9156962ee3b0d728422915925e4eefe3f335351f Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 30 Jun 2026 19:27:08 +0200 Subject: [PATCH 2/2] docs: CHANGELOG 0.3.5 + Version 0.3.5 Kostenerfassung vervollstaendigt (Filler-Uebersetzung exakt, OpenRouter-TTS geschaetzt). Version 0.3.4 -> 0.3.5. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0da82f..c915fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ Alle nennenswerten Änderungen an **Alexis**. Format lose nach [Keep a Changelog](https://keepachangelog.com/de/); Versionierung nach [SemVer](https://semver.org/lang/de/) (prä-1.0: `0.x`). +## [0.3.5] — 2026-06-30 + +### Hinzugefügt +- **Kostenerfassung vervollständigt:** + - **Filler-Übersetzung** (OpenRouter) bucht jetzt ihre Ist-Kosten (Kategorie `llm`). + - **OpenRouter-TTS** wird geschätzt erfasst (Zeichen × `openrouter_tts_usd_per_char`, + Kategorie `tts`) — der Audio-Endpunkt liefert keine inline-`usage`. Preis im Admin + live einstellbar (Default ≈ $15/1M Zeichen, an Modellpreis anpassen). + ## [0.3.4] — 2026-06-30 ### Hinzugefügt diff --git a/pyproject.toml b/pyproject.toml index a61ec5f..602ceb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "voice-assistant-gateway" -version = "0.3.4" +version = "0.3.5" description = "Modular voice assistant gateway with pluggable audio endpoints and provider adapters" readme = "README.md" requires-python = ">=3.11"