"""Durable LLM usage/cost accounting service. Records per-organization, per-user LLM usage without storing any prompt content or secrets. A simple default cost estimate (input/output per-token) is applied and can be overridden by a rate table later. """ from __future__ import annotations from typing import Optional from sqlalchemy.orm import Session from ..models.usage import UsageEvent # Default per-1K token cost estimates (USD); a rate table can supersede later. _DEFAULT_INPUT_RATE_PER_1K = 0.0025 _DEFAULT_OUTPUT_RATE_PER_1K = 0.0100 class UsageService: def __init__(self, session: Session): self.session = session def record_event( self, *, organization_id: str, user_id: Optional[str], operation: str, model: Optional[str] = None, input_tokens: int = 0, output_tokens: int = 0, ) -> str: if not isinstance(organization_id, str) or not organization_id: raise ValueError("organization_id_required") cost = ( (input_tokens / 1000) * _DEFAULT_INPUT_RATE_PER_1K + (output_tokens / 1000) * _DEFAULT_OUTPUT_RATE_PER_1K ) event = UsageEvent( organization_id=organization_id, user_id=user_id, operation=operation, model=model, input_tokens=int(input_tokens or 0), output_tokens=int(output_tokens or 0), estimated_cost=round(cost, 6), ) self.session.add(event) self.session.flush() return event.id def list_events(self, *, organization_id: str, limit: int = 100) -> list[UsageEvent]: return ( self.session.query(UsageEvent) .filter(UsageEvent.organization_id == organization_id) .order_by(UsageEvent.created_at.desc()) .limit(min(max(int(limit), 1), 1000)) .all() ) def total_cost(self, *, organization_id: str) -> float: rows = ( self.session.query(UsageEvent) .filter(UsageEvent.organization_id == organization_id) .all() ) return round(sum(row.estimated_cost for row in rows), 6)