fbd6602814
---ci--- phase: 0 milestone: v0.1 status: complete ---/ci---
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
"""Cost logging — per-session cost derivation (REQ-NFR-COST-01, D-012, TASK-04-04).
|
|
|
|
Counts LLM input/output tokens (gemma4 + deepseek-v4-flash), Deepgram audio
|
|
minutes, Cartesia/Piper characters; derives an estimated cost in cents using
|
|
cost_rates.yaml. No enforced ceiling (D-012 — pilot). The derived cost +
|
|
breakdown are stored in sessions.cost_estimated_cents / cost_breakdown_json.
|
|
|
|
v0.1 logged costs are NOT representative of at-scale per-learner cost (G-005):
|
|
Ollama tier-based pricing + Canada cloud + low volume = the most expensive
|
|
configuration. The $3/learner target requires self-hosted gemma4:e4b + Piper
|
|
(post-pilot). The logging infrastructure is the v0.1 contribution.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
_DEFAULT_RATES_PATH = Path(__file__).resolve().parent.parent / "scenarios" / "cost_rates.yaml"
|
|
|
|
|
|
@dataclass
|
|
class CostBreakdown:
|
|
"""Per-session cost inputs + derived cents."""
|
|
|
|
llm_input_tokens: int = 0
|
|
llm_output_tokens: int = 0
|
|
deepgram_audio_minutes: float = 0.0
|
|
tts_characters: int = 0
|
|
debrief_input_tokens: int = 0
|
|
debrief_output_tokens: int = 0
|
|
rates: dict[str, float] = field(default_factory=dict)
|
|
derived_cents: int = 0
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"llm_input_tokens": self.llm_input_tokens,
|
|
"llm_output_tokens": self.llm_output_tokens,
|
|
"deepgram_audio_minutes": round(self.deepgram_audio_minutes, 3),
|
|
"tts_characters": self.tts_characters,
|
|
"debrief_input_tokens": self.debrief_input_tokens,
|
|
"debrief_output_tokens": self.debrief_output_tokens,
|
|
"rates": self.rates,
|
|
"derived_cents": self.derived_cents,
|
|
}
|
|
|
|
|
|
def load_rates(path: Path | None = None) -> dict[str, float]:
|
|
"""Load cost rates from cost_rates.yaml (or defaults if absent)."""
|
|
p = path or _DEFAULT_RATES_PATH
|
|
if p.exists():
|
|
with p.open("r", encoding="utf-8") as f:
|
|
return yaml.safe_load(f) or {}
|
|
# Defaults — vendor-list prices, per-unit (pilot estimates, G-005).
|
|
return {
|
|
"gemma4_cloud_per_1k_tokens_cents": 0.5, # Ollama tier (pro plan amortized)
|
|
"deepseek_v4_flash_per_1k_tokens_cents": 1.0, # Ollama tier
|
|
"deepgram_per_audio_minute_cents": 0.43, # $0.0043/min
|
|
"cartesia_per_1k_chars_cents": 3.0, # per-char pricing
|
|
"piper_per_1k_chars_cents": 0.0, # self-hosted, $0
|
|
}
|
|
|
|
|
|
def derive_cost(
|
|
llm_input_tokens: int = 0,
|
|
llm_output_tokens: int = 0,
|
|
deepgram_audio_minutes: float = 0.0,
|
|
tts_characters: int = 0,
|
|
debrief_input_tokens: int = 0,
|
|
debrief_output_tokens: int = 0,
|
|
tts_provider: str = "cartesia",
|
|
rates: dict[str, float] | None = None,
|
|
) -> CostBreakdown:
|
|
"""Derive the per-session cost in cents from the usage inputs + rates."""
|
|
r = rates or load_rates()
|
|
|
|
# LLM role-play (gemma4:cloud).
|
|
rp_tokens = llm_input_tokens + llm_output_tokens
|
|
rp_cents = (rp_tokens / 1000.0) * r.get("gemma4_cloud_per_1k_tokens_cents", 0.5)
|
|
|
|
# Debrief (deepseek-v4-flash:cloud).
|
|
db_tokens = debrief_input_tokens + debrief_output_tokens
|
|
db_cents = (db_tokens / 1000.0) * r.get("deepseek_v4_flash_per_1k_tokens_cents", 1.0)
|
|
|
|
# ASR (Deepgram).
|
|
asr_cents = deepgram_audio_minutes * r.get("deepgram_per_audio_minute_cents", 0.43)
|
|
|
|
# TTS (Cartesia or Piper).
|
|
tts_rate_key = (
|
|
"piper_per_1k_chars_cents" if tts_provider == "piper"
|
|
else "cartesia_per_1k_chars_cents"
|
|
)
|
|
tts_cents = (tts_characters / 1000.0) * r.get(tts_rate_key, 3.0)
|
|
|
|
total = int(round(rp_cents + db_cents + asr_cents + tts_cents))
|
|
return CostBreakdown(
|
|
llm_input_tokens=llm_input_tokens,
|
|
llm_output_tokens=llm_output_tokens,
|
|
deepgram_audio_minutes=deepgram_audio_minutes,
|
|
tts_characters=tts_characters,
|
|
debrief_input_tokens=debrief_input_tokens,
|
|
debrief_output_tokens=debrief_output_tokens,
|
|
rates=r,
|
|
derived_cents=total,
|
|
)
|
|
|
|
|
|
__all__ = ["CostBreakdown", "derive_cost", "load_rates"] |