billing_store.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """计费存取:单价表、用量流水、每日额度。
  2. 口径说明(与 1.5 的关键差异):用量一律取模型返回的**真实 token 数**,
  3. 不在这一层做估算。stream 层拿不到真实 usage 时才回落估算,并在
  4. ``billing_usage`` 上留下 ``usage_source`` 痕迹(见 api/context_meter.py)。
  5. """
  6. from __future__ import annotations
  7. from typing import Any
  8. from db import db_conn
  9. from obs import get_logger
  10. logger = get_logger(__name__)
  11. # ============================================================
  12. # 单价表
  13. # ============================================================
  14. def set_pricing(
  15. model: str, input_price: float, output_price: float, currency: str = "CNY"
  16. ) -> None:
  17. """写入/覆盖某模型的单价(每 1K token)。"""
  18. existing = db_conn.query_one("SELECT model FROM billing_pricing WHERE model = ?", (model,))
  19. stamp = db_conn.fmt_dt()
  20. if existing:
  21. db_conn.execute(
  22. "UPDATE billing_pricing SET input_price = ?, output_price = ?, currency = ?, updated_at = ?"
  23. " WHERE model = ?",
  24. (input_price, output_price, currency, stamp, model),
  25. )
  26. else:
  27. db_conn.execute(
  28. "INSERT INTO billing_pricing (model, input_price, output_price, currency, updated_at)"
  29. " VALUES (?, ?, ?, ?, ?)",
  30. (model, input_price, output_price, currency, stamp),
  31. )
  32. def get_pricing(model: str) -> dict[str, Any] | None:
  33. """取某模型单价;未定价返回 None。"""
  34. return db_conn.query_one("SELECT * FROM billing_pricing WHERE model = ?", (model,))
  35. def list_pricing() -> list[dict[str, Any]]:
  36. """列出全部单价。"""
  37. return db_conn.query("SELECT * FROM billing_pricing ORDER BY model ASC")
  38. # ============================================================
  39. # 用量流水
  40. # ============================================================
  41. def record_usage(
  42. user_name: str,
  43. session_id: str,
  44. kind: str,
  45. model: str,
  46. input_tokens: int,
  47. output_tokens: int,
  48. cost: float,
  49. usage_day: str,
  50. trace_id: str = "",
  51. ) -> str:
  52. """落一条用量流水,返回流水 ID。"""
  53. usage_id = db_conn.new_id()
  54. total = int(input_tokens) + int(output_tokens)
  55. with db_conn.transaction():
  56. db_conn.execute(
  57. "INSERT INTO billing_usage (id, user_name, session_id, kind, model, input_tokens,"
  58. " output_tokens, total_tokens, cost, usage_day, trace_id, created_at)"
  59. " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
  60. (
  61. usage_id,
  62. user_name,
  63. session_id,
  64. kind,
  65. model,
  66. int(input_tokens),
  67. int(output_tokens),
  68. total,
  69. float(cost),
  70. usage_day,
  71. trace_id,
  72. db_conn.fmt_dt(),
  73. ),
  74. )
  75. _accumulate_quota(user_name, usage_day, total)
  76. return usage_id
  77. def usage_for_day(user_name: str, usage_day: str) -> dict[str, Any]:
  78. """汇总某用户某天的用量。"""
  79. row = db_conn.query_one(
  80. "SELECT COUNT(*) AS requests,"
  81. " COALESCE(SUM(input_tokens), 0) AS input_tokens,"
  82. " COALESCE(SUM(output_tokens), 0) AS output_tokens,"
  83. " COALESCE(SUM(total_tokens), 0) AS total_tokens,"
  84. " COALESCE(SUM(cost), 0) AS cost"
  85. " FROM billing_usage WHERE user_name = ? AND usage_day = ?",
  86. (user_name, usage_day),
  87. ) or {}
  88. return {
  89. "user_name": user_name,
  90. "usage_day": usage_day,
  91. "requests": int(row.get("requests", 0) or 0),
  92. "input_tokens": int(row.get("input_tokens", 0) or 0),
  93. "output_tokens": int(row.get("output_tokens", 0) or 0),
  94. "total_tokens": int(row.get("total_tokens", 0) or 0),
  95. "cost": float(row.get("cost", 0) or 0),
  96. }
  97. def recent_usage(user_name: str, limit: int = 20) -> list[dict[str, Any]]:
  98. """最近若干条用量流水。"""
  99. return db_conn.query(
  100. "SELECT * FROM billing_usage WHERE user_name = ?"
  101. " ORDER BY created_at DESC, id DESC LIMIT ?",
  102. (user_name, limit),
  103. )
  104. # ============================================================
  105. # 每日额度
  106. # ============================================================
  107. def _ensure_quota_row(user_name: str, usage_day: str) -> dict[str, Any]:
  108. """确保当日额度行存在并返回它。额度上限取自配置(0 = 不限)。"""
  109. import config
  110. row = db_conn.query_one(
  111. "SELECT * FROM daily_quota WHERE user_name = ? AND usage_day = ?",
  112. (user_name, usage_day),
  113. )
  114. if row:
  115. return row
  116. limit_tokens = int(config.DAILY_TOKEN_LIMIT)
  117. db_conn.execute(
  118. "INSERT INTO daily_quota (user_name, usage_day, limit_tokens, used_tokens, updated_at)"
  119. " VALUES (?, ?, ?, 0, ?)",
  120. (user_name, usage_day, limit_tokens, db_conn.fmt_dt()),
  121. )
  122. return {
  123. "user_name": user_name,
  124. "usage_day": usage_day,
  125. "limit_tokens": limit_tokens,
  126. "used_tokens": 0,
  127. }
  128. def _accumulate_quota(user_name: str, usage_day: str, tokens: int) -> None:
  129. """把本轮 token 累加到当日额度行。"""
  130. _ensure_quota_row(user_name, usage_day)
  131. db_conn.execute(
  132. "UPDATE daily_quota SET used_tokens = used_tokens + ?, updated_at = ?"
  133. " WHERE user_name = ? AND usage_day = ?",
  134. (int(tokens), db_conn.fmt_dt(), user_name, usage_day),
  135. )
  136. def get_quota(user_name: str, usage_day: str) -> dict[str, Any]:
  137. """读当日额度与已用。"""
  138. row = _ensure_quota_row(user_name, usage_day)
  139. limit_tokens = int(row.get("limit_tokens", 0) or 0)
  140. used = int(row.get("used_tokens", 0) or 0)
  141. return {
  142. "user_name": user_name,
  143. "usage_day": usage_day,
  144. "limit_tokens": limit_tokens,
  145. "used_tokens": used,
  146. "remaining_tokens": max(limit_tokens - used, 0) if limit_tokens > 0 else None,
  147. }
  148. def set_quota_limit(user_name: str, usage_day: str, limit_tokens: int) -> dict[str, Any]:
  149. """调整某用户某天的额度上限(0 = 不限)。"""
  150. _ensure_quota_row(user_name, usage_day)
  151. db_conn.execute(
  152. "UPDATE daily_quota SET limit_tokens = ?, updated_at = ?"
  153. " WHERE user_name = ? AND usage_day = ?",
  154. (int(limit_tokens), db_conn.fmt_dt(), user_name, usage_day),
  155. )
  156. return get_quota(user_name, usage_day)