llm_billing.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """LLM billing query service for the admin dashboard."""
  2. from __future__ import annotations
  3. from datetime import datetime, timedelta
  4. from typing import Any
  5. from zoneinfo import ZoneInfo
  6. from supply_infra.db.repositories.llm_usage_repo import LlmUsageRepository
  7. from supply_infra.db.session import get_session
  8. _SHANGHAI = ZoneInfo("Asia/Shanghai")
  9. def _today() -> datetime:
  10. return datetime.now(_SHANGHAI)
  11. def _biz_dt(day: datetime) -> str:
  12. return day.strftime("%Y%m%d")
  13. def _shift_biz_dt(biz_dt: str, days: int) -> str:
  14. base = datetime.strptime(biz_dt, "%Y%m%d").replace(tzinfo=_SHANGHAI)
  15. return _biz_dt(base + timedelta(days=days))
  16. def _format_biz_dt_label(biz_dt: str) -> str:
  17. return f"{biz_dt[0:4]}-{biz_dt[4:6]}-{biz_dt[6:8]}"
  18. def _serialize_event(row: Any) -> dict[str, Any]:
  19. return {
  20. "id": row.id,
  21. "biz_dt": row.biz_dt,
  22. "provider": row.provider,
  23. "model": row.model,
  24. "agent_name": row.agent_name,
  25. "run_id": row.run_id,
  26. "iteration": row.iteration,
  27. "prompt_tokens": row.prompt_tokens,
  28. "completion_tokens": row.completion_tokens,
  29. "total_tokens": row.total_tokens,
  30. "cost_usd": float(row.cost_usd) if row.cost_usd is not None else None,
  31. "created_at": row.created_at.isoformat(sep=" ", timespec="seconds")
  32. if row.created_at
  33. else None,
  34. }
  35. def get_billing_overview(
  36. *,
  37. start_dt: str | None = None,
  38. end_dt: str | None = None,
  39. recent_limit: int = 50,
  40. ) -> dict[str, Any]:
  41. """Aggregate LLM cost for a date range with agent/model breakdowns."""
  42. today = _today()
  43. today_dt = _biz_dt(today)
  44. end = end_dt or today_dt
  45. start = start_dt or _shift_biz_dt(end, -29)
  46. if start > end:
  47. start, end = end, start
  48. yesterday_dt = _shift_biz_dt(today_dt, -1)
  49. week_start = _shift_biz_dt(today_dt, -6)
  50. with get_session() as session:
  51. repo = LlmUsageRepository(session)
  52. daily = repo.summarize_daily(start_dt=start, end_dt=end)
  53. by_agent = repo.breakdown_by_agent(start_dt=start, end_dt=end)
  54. by_model = repo.breakdown_by_model(start_dt=start, end_dt=end)
  55. recent = repo.list_recent_events(start_dt=start, end_dt=end, limit=recent_limit)
  56. # Fill missing days so charts are continuous.
  57. daily_map = {item["biz_dt"]: item for item in daily}
  58. filled_daily: list[dict[str, Any]] = []
  59. cursor = start
  60. while cursor <= end:
  61. item = daily_map.get(
  62. cursor,
  63. {
  64. "biz_dt": cursor,
  65. "call_count": 0,
  66. "prompt_tokens": 0,
  67. "completion_tokens": 0,
  68. "total_tokens": 0,
  69. "total_cost_usd": 0.0,
  70. "missing_cost_count": 0,
  71. },
  72. )
  73. filled_daily.append({**item, "label": _format_biz_dt_label(cursor)})
  74. cursor = _shift_biz_dt(cursor, 1)
  75. range_cost = sum(item["total_cost_usd"] for item in filled_daily)
  76. range_calls = sum(item["call_count"] for item in filled_daily)
  77. range_tokens = sum(item["total_tokens"] for item in filled_daily)
  78. today_item = daily_map.get(today_dt)
  79. yesterday_item = daily_map.get(yesterday_dt)
  80. week_cost = sum(
  81. item["total_cost_usd"]
  82. for item in filled_daily
  83. if week_start <= item["biz_dt"] <= today_dt
  84. )
  85. return {
  86. "timezone": "Asia/Shanghai",
  87. "currency": "USD",
  88. "range": {
  89. "start_dt": start,
  90. "end_dt": end,
  91. "total_cost_usd": range_cost,
  92. "call_count": range_calls,
  93. "total_tokens": range_tokens,
  94. },
  95. "summary": {
  96. "today_biz_dt": today_dt,
  97. "today_cost_usd": float(today_item["total_cost_usd"]) if today_item else 0.0,
  98. "today_call_count": int(today_item["call_count"]) if today_item else 0,
  99. "yesterday_cost_usd": float(yesterday_item["total_cost_usd"])
  100. if yesterday_item
  101. else 0.0,
  102. "week_cost_usd": week_cost,
  103. "range_cost_usd": range_cost,
  104. },
  105. "daily": filled_daily,
  106. "by_agent": by_agent,
  107. "by_model": by_model,
  108. "recent_events": [_serialize_event(row) for row in recent],
  109. }