from datetime import date, datetime, timedelta from decimal import Decimal from typing import Optional from sqlalchemy import select, func, cast, Date from sqlalchemy.ext.asyncio import AsyncSession from app.models import UsageLog, ApiKey class UsageService: @staticmethod async def summary(db: AsyncSession, user_id: str) -> dict: today_start = datetime.combine(date.today(), datetime.min.time()) month_start = today_start.replace(day=1) # Today today_row = (await db.execute( select( func.coalesce(func.sum(UsageLog.total_tokens), 0), func.coalesce(func.sum(UsageLog.cost), Decimal("0")), ).where(UsageLog.user_id == user_id, UsageLog.request_time >= today_start) )).one() # This month month_row = (await db.execute( select( func.coalesce(func.sum(UsageLog.total_tokens), 0), func.coalesce(func.sum(UsageLog.cost), Decimal("0")), func.count(), ).where(UsageLog.user_id == user_id, UsageLog.request_time >= month_start) )).one() return { "today_tokens": int(today_row[0]), "today_cost": today_row[1], "month_tokens": int(month_row[0]), "month_cost": month_row[1], "total_requests": int(month_row[2]), } @staticmethod async def daily( db: AsyncSession, user_id: str, start: Optional[date] = None, end: Optional[date] = None, ) -> list: if not start: start = date.today() - timedelta(days=29) if not end: end = date.today() day_col = cast(UsageLog.request_time, Date).label("day") result = await db.execute( select( day_col, func.coalesce(func.sum(UsageLog.total_tokens), 0), func.coalesce(func.sum(UsageLog.cost), Decimal("0")), func.count(), ) .where( UsageLog.user_id == user_id, cast(UsageLog.request_time, Date) >= start, cast(UsageLog.request_time, Date) <= end, ) .group_by(day_col) .order_by(day_col) ) return [ {"date": row[0], "total_tokens": int(row[1]), "cost": row[2], "requests": int(row[3])} for row in result.all() ] @staticmethod async def by_model(db: AsyncSession, user_id: str) -> list: result = await db.execute( select( UsageLog.model, func.coalesce(func.sum(UsageLog.total_tokens), 0), func.coalesce(func.sum(UsageLog.cost), Decimal("0")), func.count(), ) .where(UsageLog.user_id == user_id) .group_by(UsageLog.model) .order_by(func.sum(UsageLog.cost).desc()) ) return [ {"model": row[0], "total_tokens": int(row[1]), "cost": row[2], "requests": int(row[3])} for row in result.all() ] @staticmethod async def by_key(db: AsyncSession, user_id: str) -> list: result = await db.execute( select( UsageLog.key_id, ApiKey.name, ApiKey.key_prefix, ApiKey.key_suffix, func.coalesce(func.sum(UsageLog.total_tokens), 0), func.coalesce(func.sum(UsageLog.cost), Decimal("0")), func.count(), ) .join(ApiKey, UsageLog.key_id == ApiKey.id) .where(UsageLog.user_id == user_id) .group_by(UsageLog.key_id, ApiKey.name, ApiKey.key_prefix, ApiKey.key_suffix) .order_by(func.sum(UsageLog.cost).desc()) ) return [ { "key_id": row[0], "key_name": row[1] or "", "key_prefix": row[2], "key_suffix": row[3], "total_tokens": int(row[4]), "cost": row[5], "requests": int(row[6]), } for row in result.all() ] @staticmethod async def logs( db: AsyncSession, user_id: str, page: int = 1, size: int = 20, model: Optional[str] = None, key_id: Optional[str] = None, start: Optional[date] = None, end: Optional[date] = None, ) -> list: q = select(UsageLog).where(UsageLog.user_id == user_id) if model: q = q.where(UsageLog.model == model) if key_id: q = q.where(UsageLog.key_id == key_id) if start: q = q.where(UsageLog.request_time >= datetime.combine(start, datetime.min.time())) if end: q = q.where(UsageLog.request_time < datetime.combine(end + timedelta(days=1), datetime.min.time())) q = q.order_by(UsageLog.request_time.desc()).offset((page - 1) * size).limit(size) result = await db.execute(q) return result.scalars().all()