"""Admin leaderboard stats routes.""" from __future__ import annotations import json from datetime import date from typing import Any, Literal from fastapi import APIRouter, Depends, Query, Request from sqlalchemy import func from sqlalchemy.orm import Session from src.api.base.admin_adapter import AdminApiAdapter from src.api.base.context import ApiRequestContext from src.clients.redis_client import get_redis_client_sync from src.config.constants import CacheTTL from src.core.enums import UserRole from src.database import get_db from src.models.database import ( ApiKey, StatsDailyApiKey, StatsDailyModel, StatsUserDaily, Usage, User, ) from src.services.system.time_range import TimeRangeParams from .common import ( _apply_admin_default_range, _apply_usage_time_segments, _build_cache_key, _build_time_range_params, _is_today_range, _metric_order, _split_daily_and_usage_segments, _union_queries, pipeline, ) router = APIRouter() class AdminUserLeaderboardAdapter(AdminApiAdapter): def __init__( self, time_range: TimeRangeParams | None, metric: Literal["requests", "tokens", "cost"], order: Literal["asc", "desc"], limit: int, offset: int, provider_name: str | None, model: str | None, include_inactive: bool, exclude_admin: bool, ) -> None: self.time_range = _apply_admin_default_range(time_range) self.metric = metric self.order = order self.limit = limit self.offset = offset self.provider_name = provider_name self.model = model self.include_inactive = include_inactive self.exclude_admin = exclude_admin async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] db = context.db cacheable = not _is_today_range(self.time_range) redis_client = get_redis_client_sync() cache_key = None if cacheable and redis_client: cache_key = _build_cache_key( "users", self.metric, self.time_range, { "order": self.order, "limit": self.limit, "offset": self.offset, "provider_name": self.provider_name, "model": self.model, "include_inactive": self.include_inactive, "exclude_admin": self.exclude_admin, }, ) cached = await redis_client.get(cache_key) if cached: try: return json.loads(cached) except Exception: pass use_daily = self.time_range is not None and not self.provider_name and not self.model daily_range, usage_segments = _split_daily_and_usage_segments(self.time_range, use_daily) daily_query = None if daily_range: daily_query = ( db.query( StatsUserDaily.user_id.label("entity_id"), func.sum(StatsUserDaily.total_requests).label("requests"), func.sum( StatsUserDaily.input_tokens + StatsUserDaily.output_tokens + StatsUserDaily.cache_creation_tokens + StatsUserDaily.cache_read_tokens ).label("tokens"), func.sum(StatsUserDaily.total_cost).label("cost"), ) .filter(StatsUserDaily.date >= daily_range[0], StatsUserDaily.date < daily_range[1]) .group_by(StatsUserDaily.user_id) ) usage_query = db.query( Usage.user_id.label("entity_id"), func.count(Usage.id).label("requests"), func.sum( Usage.input_tokens + Usage.output_tokens + Usage.cache_creation_input_tokens + Usage.cache_read_input_tokens ).label("tokens"), func.sum(Usage.total_cost_usd).label("cost"), ).filter( Usage.user_id.isnot(None), Usage.status.notin_(["pending", "streaming"]), Usage.provider_name.notin_(["unknown", "pending"]), ) if self.provider_name: usage_query = usage_query.filter(Usage.provider_name == self.provider_name) if self.model: usage_query = usage_query.filter(Usage.model == self.model) usage_query = _apply_usage_time_segments(usage_query, usage_segments) if usage_query is not None: usage_query = usage_query.group_by(Usage.user_id) union_query = _union_queries([daily_query, usage_query]) if union_query is None: return { "items": [], "total": 0, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } union_subq = union_query.subquery() agg_subq = ( db.query( union_subq.c.entity_id.label("entity_id"), func.sum(union_subq.c.requests).label("requests"), func.sum(union_subq.c.tokens).label("tokens"), func.sum(union_subq.c.cost).label("cost"), ) .group_by(union_subq.c.entity_id) .subquery() ) base_query = ( db.query( User.id.label("id"), User.username, User.email, agg_subq.c.requests, agg_subq.c.tokens, agg_subq.c.cost, ) .join(agg_subq, agg_subq.c.entity_id == User.id) .filter(User.is_deleted.is_(False)) ) if not self.include_inactive: base_query = base_query.filter(User.is_active.is_(True)) if self.exclude_admin: base_query = base_query.filter(User.role != UserRole.ADMIN) metric_expr = { "requests": agg_subq.c.requests, "tokens": agg_subq.c.tokens, "cost": agg_subq.c.cost, }[self.metric] order_expr = _metric_order(self.metric, self.order, metric_expr) rank_expr = func.dense_rank().over(order_by=order_expr).label("rank") total = db.query(func.count()).select_from(base_query.subquery()).scalar() or 0 rows = ( base_query.add_columns(rank_expr, metric_expr.label("metric_value")) .order_by(order_expr) .offset(self.offset) .limit(self.limit) .all() ) items = [] for row in rows: name = row.username or row.email or str(row.id) value = row.metric_value or 0 if self.metric in {"requests", "tokens"}: value = int(value) else: value = float(value) items.append( { "rank": int(row.rank), "id": row.id, "name": name, "value": value, "requests": int(row.requests or 0), "tokens": int(row.tokens or 0), "cost": float(row.cost or 0.0), } ) context.add_audit_metadata( action="leaderboard_users", start_date=self.time_range.start_date.isoformat() if self.time_range else None, end_date=self.time_range.end_date.isoformat() if self.time_range else None, preset=self.time_range.preset if self.time_range else None, timezone=self.time_range.timezone if self.time_range else None, metric=self.metric, order=self.order, limit=self.limit, offset=self.offset, provider_name=self.provider_name, model=self.model, include_inactive=self.include_inactive, exclude_admin=self.exclude_admin, result_count=len(items), total=total, ) result = { "items": items, "total": total, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } if cacheable and redis_client and cache_key: try: await redis_client.setex( cache_key, CacheTTL.ADMIN_LEADERBOARD, json.dumps(result, ensure_ascii=False) ) except Exception: pass return result class AdminApiKeyLeaderboardAdapter(AdminApiAdapter): def __init__( self, time_range: TimeRangeParams | None, metric: Literal["requests", "tokens", "cost"], order: Literal["asc", "desc"], limit: int, offset: int, provider_name: str | None, model: str | None, include_inactive: bool, exclude_admin: bool, ) -> None: self.time_range = _apply_admin_default_range(time_range) self.metric = metric self.order = order self.limit = limit self.offset = offset self.provider_name = provider_name self.model = model self.include_inactive = include_inactive self.exclude_admin = exclude_admin async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] db = context.db cacheable = not _is_today_range(self.time_range) redis_client = get_redis_client_sync() cache_key = None if cacheable and redis_client: cache_key = _build_cache_key( "api_keys", self.metric, self.time_range, { "order": self.order, "limit": self.limit, "offset": self.offset, "provider_name": self.provider_name, "model": self.model, "include_inactive": self.include_inactive, "exclude_admin": self.exclude_admin, }, ) cached = await redis_client.get(cache_key) if cached: try: return json.loads(cached) except Exception: pass use_daily = self.time_range is not None and not self.provider_name and not self.model daily_range, usage_segments = _split_daily_and_usage_segments(self.time_range, use_daily) daily_query = None if daily_range: daily_query = ( db.query( StatsDailyApiKey.api_key_id.label("entity_id"), func.sum(StatsDailyApiKey.total_requests).label("requests"), func.sum( StatsDailyApiKey.input_tokens + StatsDailyApiKey.output_tokens + StatsDailyApiKey.cache_creation_tokens + StatsDailyApiKey.cache_read_tokens ).label("tokens"), func.sum(StatsDailyApiKey.total_cost).label("cost"), ) .filter( StatsDailyApiKey.date >= daily_range[0], StatsDailyApiKey.date < daily_range[1] ) .group_by(StatsDailyApiKey.api_key_id) ) usage_query = db.query( Usage.api_key_id.label("entity_id"), func.count(Usage.id).label("requests"), func.sum( Usage.input_tokens + Usage.output_tokens + Usage.cache_creation_input_tokens + Usage.cache_read_input_tokens ).label("tokens"), func.sum(Usage.total_cost_usd).label("cost"), ).filter( Usage.api_key_id.isnot(None), Usage.status.notin_(["pending", "streaming"]), Usage.provider_name.notin_(["unknown", "pending"]), ) if self.provider_name: usage_query = usage_query.filter(Usage.provider_name == self.provider_name) if self.model: usage_query = usage_query.filter(Usage.model == self.model) usage_query = _apply_usage_time_segments(usage_query, usage_segments) if usage_query is None: return { "items": [], "total": 0, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } usage_query = usage_query.group_by(Usage.api_key_id) union_query = _union_queries([daily_query, usage_query]) if union_query is None: return { "items": [], "total": 0, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } union_subq = union_query.subquery() agg_subq = ( db.query( union_subq.c.entity_id.label("entity_id"), func.sum(union_subq.c.requests).label("requests"), func.sum(union_subq.c.tokens).label("tokens"), func.sum(union_subq.c.cost).label("cost"), ) .group_by(union_subq.c.entity_id) .subquery() ) base_query = ( db.query( ApiKey, User, agg_subq.c.requests, agg_subq.c.tokens, agg_subq.c.cost, ) .join(agg_subq, agg_subq.c.entity_id == ApiKey.id) .join(User, User.id == ApiKey.user_id) .filter(User.is_deleted.is_(False)) ) if not self.include_inactive: base_query = base_query.filter(ApiKey.is_active.is_(True)) if self.exclude_admin: base_query = base_query.filter(User.role != UserRole.ADMIN) metric_expr = { "requests": agg_subq.c.requests, "tokens": agg_subq.c.tokens, "cost": agg_subq.c.cost, }[self.metric] order_expr = _metric_order(self.metric, self.order, metric_expr) rank_expr = func.dense_rank().over(order_by=order_expr).label("rank") total = db.query(func.count()).select_from(base_query.subquery()).scalar() or 0 rows = ( base_query.add_columns(rank_expr, metric_expr.label("metric_value")) .order_by(order_expr) .offset(self.offset) .limit(self.limit) .all() ) items = [] for row in rows: api_key = row.ApiKey name = api_key.name or api_key.get_display_key() value = row.metric_value or 0 if self.metric in {"requests", "tokens"}: value = int(value) else: value = float(value) items.append( { "rank": int(row.rank), "id": api_key.id, "name": name, "value": value, "requests": int(row.requests or 0), "tokens": int(row.tokens or 0), "cost": float(row.cost or 0.0), } ) context.add_audit_metadata( action="leaderboard_api_keys", start_date=self.time_range.start_date.isoformat() if self.time_range else None, end_date=self.time_range.end_date.isoformat() if self.time_range else None, preset=self.time_range.preset if self.time_range else None, timezone=self.time_range.timezone if self.time_range else None, metric=self.metric, order=self.order, limit=self.limit, offset=self.offset, provider_name=self.provider_name, model=self.model, include_inactive=self.include_inactive, exclude_admin=self.exclude_admin, result_count=len(items), total=total, ) result = { "items": items, "total": total, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } if cacheable and redis_client and cache_key: try: await redis_client.setex( cache_key, CacheTTL.ADMIN_LEADERBOARD, json.dumps(result, ensure_ascii=False) ) except Exception: pass return result class AdminModelLeaderboardAdapter(AdminApiAdapter): def __init__( self, time_range: TimeRangeParams | None, metric: Literal["requests", "tokens", "cost"], order: Literal["asc", "desc"], limit: int, offset: int, provider_name: str | None, model: str | None, ) -> None: self.time_range = _apply_admin_default_range(time_range) self.metric = metric self.order = order self.limit = limit self.offset = offset self.provider_name = provider_name self.model = model async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] db = context.db cacheable = not _is_today_range(self.time_range) redis_client = get_redis_client_sync() cache_key = None if cacheable and redis_client: cache_key = _build_cache_key( "models", self.metric, self.time_range, { "order": self.order, "limit": self.limit, "offset": self.offset, "provider_name": self.provider_name, "model": self.model, }, ) cached = await redis_client.get(cache_key) if cached: try: return json.loads(cached) except Exception: pass use_daily = self.time_range is not None and not self.provider_name daily_range, usage_segments = _split_daily_and_usage_segments(self.time_range, use_daily) daily_query = None if daily_range: daily_query = ( db.query( StatsDailyModel.model.label("entity_id"), func.sum(StatsDailyModel.total_requests).label("requests"), func.sum( StatsDailyModel.input_tokens + StatsDailyModel.output_tokens + StatsDailyModel.cache_creation_tokens + StatsDailyModel.cache_read_tokens ).label("tokens"), func.sum(StatsDailyModel.total_cost).label("cost"), ) .filter( StatsDailyModel.date >= daily_range[0], StatsDailyModel.date < daily_range[1] ) .group_by(StatsDailyModel.model) ) if self.model: daily_query = daily_query.filter(StatsDailyModel.model == self.model) usage_query = db.query( Usage.model.label("entity_id"), func.count(Usage.id).label("requests"), func.sum( Usage.input_tokens + Usage.output_tokens + Usage.cache_creation_input_tokens + Usage.cache_read_input_tokens ).label("tokens"), func.sum(Usage.total_cost_usd).label("cost"), ).filter( Usage.status.notin_(["pending", "streaming"]), Usage.provider_name.notin_(["unknown", "pending"]), ) if self.provider_name: usage_query = usage_query.filter(Usage.provider_name == self.provider_name) if self.model: usage_query = usage_query.filter(Usage.model == self.model) usage_query = _apply_usage_time_segments(usage_query, usage_segments) if usage_query is not None: usage_query = usage_query.group_by(Usage.model) union_query = _union_queries([daily_query, usage_query]) if union_query is None: return { "items": [], "total": 0, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } union_subq = union_query.subquery() agg_subq = ( db.query( union_subq.c.entity_id.label("entity_id"), func.sum(union_subq.c.requests).label("requests"), func.sum(union_subq.c.tokens).label("tokens"), func.sum(union_subq.c.cost).label("cost"), ) .group_by(union_subq.c.entity_id) .subquery() ) base_query = db.query( agg_subq.c.entity_id.label("id"), agg_subq.c.entity_id.label("name"), agg_subq.c.requests, agg_subq.c.tokens, agg_subq.c.cost, ) metric_expr = { "requests": agg_subq.c.requests, "tokens": agg_subq.c.tokens, "cost": agg_subq.c.cost, }[self.metric] order_expr = _metric_order(self.metric, self.order, metric_expr) rank_expr = func.dense_rank().over(order_by=order_expr).label("rank") total = db.query(func.count()).select_from(base_query.subquery()).scalar() or 0 rows = ( base_query.add_columns(rank_expr, metric_expr.label("metric_value")) .order_by(order_expr) .offset(self.offset) .limit(self.limit) .all() ) items = [] for row in rows: value = row.metric_value or 0 if self.metric in {"requests", "tokens"}: value = int(value) else: value = float(value) items.append( { "rank": int(row.rank), "id": row.id, "name": row.name, "value": value, "requests": int(row.requests or 0), "tokens": int(row.tokens or 0), "cost": float(row.cost or 0.0), } ) context.add_audit_metadata( action="leaderboard_models", start_date=self.time_range.start_date.isoformat() if self.time_range else None, end_date=self.time_range.end_date.isoformat() if self.time_range else None, preset=self.time_range.preset if self.time_range else None, timezone=self.time_range.timezone if self.time_range else None, metric=self.metric, order=self.order, limit=self.limit, offset=self.offset, provider_name=self.provider_name, model=self.model, result_count=len(items), total=total, ) result = { "items": items, "total": total, "metric": self.metric, "start_date": self.time_range.start_date.isoformat() if self.time_range else None, "end_date": self.time_range.end_date.isoformat() if self.time_range else None, } if cacheable and redis_client and cache_key: try: await redis_client.setex( cache_key, CacheTTL.ADMIN_LEADERBOARD, json.dumps(result, ensure_ascii=False) ) except Exception: pass return result @router.get("/leaderboard/users") async def get_user_leaderboard( request: Request, db: Session = Depends(get_db), start_date: date | None = Query(None), end_date: date | None = Query(None), preset: str | None = Query(None), timezone_name: str | None = Query(None, alias="timezone"), tz_offset_minutes: int | None = Query(0), metric: Literal["requests", "tokens", "cost"] = Query("requests"), order: Literal["desc", "asc"] = Query("desc"), limit: int = Query(10, ge=1, le=100), offset: int = Query(0, ge=0), provider_name: str | None = Query(None), model: str | None = Query(None), include_inactive: bool = Query(False), exclude_admin: bool = Query(False), ) -> Any: time_range = _build_time_range_params( start_date, end_date, preset, timezone_name, tz_offset_minutes ) adapter = AdminUserLeaderboardAdapter( time_range=time_range, metric=metric, order=order, limit=limit, offset=offset, provider_name=provider_name, model=model, include_inactive=include_inactive, exclude_admin=exclude_admin, ) return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode) @router.get("/leaderboard/api-keys") async def get_api_key_leaderboard( request: Request, db: Session = Depends(get_db), start_date: date | None = Query(None), end_date: date | None = Query(None), preset: str | None = Query(None), timezone_name: str | None = Query(None, alias="timezone"), tz_offset_minutes: int | None = Query(0), metric: Literal["requests", "tokens", "cost"] = Query("requests"), order: Literal["desc", "asc"] = Query("desc"), limit: int = Query(10, ge=1, le=100), offset: int = Query(0, ge=0), provider_name: str | None = Query(None), model: str | None = Query(None), include_inactive: bool = Query(False), exclude_admin: bool = Query(False), ) -> Any: time_range = _build_time_range_params( start_date, end_date, preset, timezone_name, tz_offset_minutes ) adapter = AdminApiKeyLeaderboardAdapter( time_range=time_range, metric=metric, order=order, limit=limit, offset=offset, provider_name=provider_name, model=model, include_inactive=include_inactive, exclude_admin=exclude_admin, ) return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode) @router.get("/leaderboard/models") async def get_model_leaderboard( request: Request, db: Session = Depends(get_db), start_date: date | None = Query(None), end_date: date | None = Query(None), preset: str | None = Query(None), timezone_name: str | None = Query(None, alias="timezone"), tz_offset_minutes: int | None = Query(0), metric: Literal["requests", "tokens", "cost"] = Query("requests"), order: Literal["desc", "asc"] = Query("desc"), limit: int = Query(10, ge=1, le=100), offset: int = Query(0, ge=0), provider_name: str | None = Query(None), model: str | None = Query(None), ) -> Any: time_range = _build_time_range_params( start_date, end_date, preset, timezone_name, tz_offset_minutes ) adapter = AdminModelLeaderboardAdapter( time_range=time_range, metric=metric, order=order, limit=limit, offset=offset, provider_name=provider_name, model=model, ) return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)