pricing.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """模型单价与费用计算。
  2. 单价含义:每 1K token 的价格(与 config 表 billing_pricing 的列注释一致)。
  3. 骨架自带一组种子单价,启动时写入 billing_pricing(不覆盖已存在的人工调价)。
  4. """
  5. from __future__ import annotations
  6. import config
  7. from db import billing_store, db_conn
  8. from obs import get_logger
  9. logger = get_logger(__name__)
  10. def enabled() -> bool:
  11. return config.get_bool('BILLING_ENABLED', True)
  12. class UnpricedModelError(RuntimeError):
  13. """模型在 billing_pricing 里没有单价,且不允许按 0 计费。"""
  14. # 种子单价:仅为了让骨架开箱可算费,**不是**真实报价,上线前必须按实际
  15. # 供应商账单校准。单位:元 / 1K token。
  16. SEED_PRICING: dict[str, tuple[float, float]] = {
  17. "deepseek-flash": (0.0005, 0.0015),
  18. "deepseek-v4-flash": (0.0005, 0.0015),
  19. "deepseek-v4-pro": (0.002, 0.008),
  20. "deepseek-chat": (0.001, 0.002),
  21. "deepseek-reasoner": (0.001, 0.002),
  22. "gpt-4o-mini": (0.001, 0.004),
  23. # 用户提供的平台报价:输入 0.4 元 / M,输出 3.2 元 / M,换算为每 1K token。
  24. "qwen3.5-35b-a3b": (0.0004, 0.0032),
  25. }
  26. def seed_default_pricing() -> int:
  27. """把种子单价写入单价表;已存在的模型不动。返回新写入条数。"""
  28. if not enabled():
  29. logger.info('计费已关闭,仅记录 Token 用量,不要求模型单价')
  30. return 0
  31. created = 0
  32. for model, (input_price, output_price) in SEED_PRICING.items():
  33. if billing_store.get_pricing(model) is None:
  34. billing_store.set_pricing(model, input_price, output_price)
  35. created += 1
  36. if created:
  37. logger.info("单价表初始化:新写入 %d 条", created)
  38. return created
  39. def compute_cost(model: str, input_tokens: int, output_tokens: int) -> tuple[float, str]:
  40. """按单价表算本轮费用。
  41. 返回 ``(cost, currency)``。模型未定价时:
  42. - 默认 fail-closed —— 抛 :class:`UnpricedModelError`,避免「静默免费」
  43. - ``BILLING_ALLOW_UNPRICED=1`` 时降级为 0 价并告警(便于本地开发)
  44. """
  45. if not enabled():
  46. return 0.0, ''
  47. pricing = billing_store.get_pricing(model)
  48. if pricing is None:
  49. if config.get_bool("BILLING_ALLOW_UNPRICED", False):
  50. logger.warning("模型 %s 未定价,按 0 计费(BILLING_ALLOW_UNPRICED=1)", model)
  51. return 0.0, "CNY"
  52. raise UnpricedModelError(
  53. f"模型 {model!r} 未在 billing_pricing 表中定价;"
  54. f"请补单价,或设 BILLING_ALLOW_UNPRICED=1 临时按 0 计费"
  55. )
  56. input_price = float(pricing.get("input_price") or 0)
  57. output_price = float(pricing.get("output_price") or 0)
  58. cost = (int(input_tokens) / 1000.0) * input_price + (
  59. int(output_tokens) / 1000.0
  60. ) * output_price
  61. return round(cost, 8), str(pricing.get("currency") or "CNY")
  62. def pricing_table() -> list[dict]:
  63. """当前单价表(供 /api/usage 展示)。"""
  64. if not enabled():
  65. return []
  66. rows = billing_store.list_pricing()
  67. return [
  68. {
  69. "model": row.get("model"),
  70. "input_price": float(row.get("input_price") or 0),
  71. "output_price": float(row.get("output_price") or 0),
  72. "currency": row.get("currency"),
  73. "updated_at": row.get("updated_at"),
  74. }
  75. for row in rows
  76. ]
  77. __all__ = [
  78. "SEED_PRICING",
  79. "UnpricedModelError",
  80. "compute_cost",
  81. "pricing_table",
  82. "seed_default_pricing",
  83. "db_conn",
  84. ]