"""LLM billing query service for the admin dashboard.""" from __future__ import annotations from datetime import datetime, timedelta from typing import Any from zoneinfo import ZoneInfo from supply_infra.db.repositories.llm_usage_repo import LlmUsageRepository from supply_infra.db.session import get_session _SHANGHAI = ZoneInfo("Asia/Shanghai") def _today() -> datetime: return datetime.now(_SHANGHAI) def _biz_dt(day: datetime) -> str: return day.strftime("%Y%m%d") def _shift_biz_dt(biz_dt: str, days: int) -> str: base = datetime.strptime(biz_dt, "%Y%m%d").replace(tzinfo=_SHANGHAI) return _biz_dt(base + timedelta(days=days)) def _format_biz_dt_label(biz_dt: str) -> str: return f"{biz_dt[0:4]}-{biz_dt[4:6]}-{biz_dt[6:8]}" def _serialize_event(row: Any) -> dict[str, Any]: return { "id": row.id, "biz_dt": row.biz_dt, "provider": row.provider, "model": row.model, "agent_name": row.agent_name, "run_id": row.run_id, "iteration": row.iteration, "prompt_tokens": row.prompt_tokens, "completion_tokens": row.completion_tokens, "total_tokens": row.total_tokens, "cost_usd": float(row.cost_usd) if row.cost_usd is not None else None, "created_at": row.created_at.isoformat(sep=" ", timespec="seconds") if row.created_at else None, } def get_billing_overview( *, start_dt: str | None = None, end_dt: str | None = None, recent_limit: int = 50, ) -> dict[str, Any]: """Aggregate LLM cost for a date range with agent/model breakdowns.""" today = _today() today_dt = _biz_dt(today) end = end_dt or today_dt start = start_dt or _shift_biz_dt(end, -29) if start > end: start, end = end, start yesterday_dt = _shift_biz_dt(today_dt, -1) week_start = _shift_biz_dt(today_dt, -6) with get_session() as session: repo = LlmUsageRepository(session) daily = repo.summarize_daily(start_dt=start, end_dt=end) by_agent = repo.breakdown_by_agent(start_dt=start, end_dt=end) by_model = repo.breakdown_by_model(start_dt=start, end_dt=end) recent = repo.list_recent_events(start_dt=start, end_dt=end, limit=recent_limit) # Fill missing days so charts are continuous. daily_map = {item["biz_dt"]: item for item in daily} filled_daily: list[dict[str, Any]] = [] cursor = start while cursor <= end: item = daily_map.get( cursor, { "biz_dt": cursor, "call_count": 0, "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "total_cost_usd": 0.0, "missing_cost_count": 0, }, ) filled_daily.append({**item, "label": _format_biz_dt_label(cursor)}) cursor = _shift_biz_dt(cursor, 1) range_cost = sum(item["total_cost_usd"] for item in filled_daily) range_calls = sum(item["call_count"] for item in filled_daily) range_tokens = sum(item["total_tokens"] for item in filled_daily) today_item = daily_map.get(today_dt) yesterday_item = daily_map.get(yesterday_dt) week_cost = sum( item["total_cost_usd"] for item in filled_daily if week_start <= item["biz_dt"] <= today_dt ) return { "timezone": "Asia/Shanghai", "currency": "USD", "range": { "start_dt": start, "end_dt": end, "total_cost_usd": range_cost, "call_count": range_calls, "total_tokens": range_tokens, }, "summary": { "today_biz_dt": today_dt, "today_cost_usd": float(today_item["total_cost_usd"]) if today_item else 0.0, "today_call_count": int(today_item["call_count"]) if today_item else 0, "yesterday_cost_usd": float(yesterday_item["total_cost_usd"]) if yesterday_item else 0.0, "week_cost_usd": week_cost, "range_cost_usd": range_cost, }, "daily": filled_daily, "by_agent": by_agent, "by_model": by_model, "recent_events": [_serialize_event(row) for row in recent], }