billing_store.py 6.7 KB

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