pricing.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. """
  2. LLM 定价计算器
  3. 使用策略模式,支持:
  4. 1. YAML 配置文件定义模型价格
  5. 2. 不同 token 类型的差异化定价(input/output/reasoning/cache)
  6. 3. 自动匹配模型(支持通配符)
  7. 4. 费用计算
  8. 定价单位:美元 / 1M tokens
  9. """
  10. import os
  11. import re
  12. from dataclasses import dataclass, field
  13. from pathlib import Path
  14. from typing import Dict, Any, Optional, List
  15. import yaml
  16. from .usage import TokenUsage
  17. @dataclass
  18. class ModelPricing:
  19. """
  20. 单个模型的定价配置
  21. 所有价格单位:美元 / 1M tokens
  22. """
  23. model: str # 模型名称(支持通配符 *)
  24. input_price: float = 0.0 # 输入 token 价格
  25. output_price: float = 0.0 # 输出 token 价格
  26. # 可选的差异化定价
  27. reasoning_price: Optional[float] = None # 推理 token 价格(默认 = output_price)
  28. cache_creation_price: Optional[float] = None # 缓存创建价格(默认 = input_price * 1.25)
  29. cache_read_price: Optional[float] = None # 缓存读取价格(默认 = input_price * 0.1)
  30. # 元数据
  31. provider: Optional[str] = None # 提供商
  32. description: Optional[str] = None # 描述
  33. def get_reasoning_price(self) -> float:
  34. """获取推理 token 价格"""
  35. return self.reasoning_price if self.reasoning_price is not None else self.output_price
  36. def get_cache_creation_price(self) -> float:
  37. """获取缓存创建价格"""
  38. return self.cache_creation_price if self.cache_creation_price is not None else self.input_price * 1.25
  39. def get_cache_read_price(self) -> float:
  40. """获取缓存读取价格"""
  41. return self.cache_read_price if self.cache_read_price is not None else self.input_price * 0.1
  42. def calculate_cost(self, usage: TokenUsage) -> float:
  43. """
  44. 计算费用
  45. Args:
  46. usage: Token 使用量
  47. Returns:
  48. 费用(美元)
  49. """
  50. cost = 0.0
  51. # 基础输入费用
  52. # 如果有缓存,需要分开计算
  53. if usage.cache_read_tokens or usage.cache_creation_tokens:
  54. # 普通输入 = 总输入 - 缓存读取(缓存读取部分单独计价)
  55. regular_input = usage.input_tokens - usage.cache_read_tokens
  56. cost += (regular_input / 1_000_000) * self.input_price
  57. cost += (usage.cache_read_tokens / 1_000_000) * self.get_cache_read_price()
  58. cost += (usage.cache_creation_tokens / 1_000_000) * self.get_cache_creation_price()
  59. else:
  60. cost += (usage.input_tokens / 1_000_000) * self.input_price
  61. # 输出费用
  62. # 如果有 reasoning tokens,需要分开计算
  63. if usage.reasoning_tokens:
  64. # 普通输出 = 总输出 - reasoning(reasoning 部分单独计价)
  65. regular_output = usage.output_tokens - usage.reasoning_tokens
  66. cost += (regular_output / 1_000_000) * self.output_price
  67. cost += (usage.reasoning_tokens / 1_000_000) * self.get_reasoning_price()
  68. else:
  69. cost += (usage.output_tokens / 1_000_000) * self.output_price
  70. return cost
  71. def matches(self, model_name: str) -> bool:
  72. """
  73. 检查模型名称是否匹配
  74. 支持通配符:
  75. - "gpt-4*" 匹配 "gpt-4", "gpt-4-turbo", "gpt-4o" 等
  76. - "claude-3-*" 匹配 "claude-3-opus", "claude-3-sonnet" 等
  77. """
  78. pattern = self.model.replace("*", ".*")
  79. return bool(re.match(f"^{pattern}$", model_name, re.IGNORECASE))
  80. @classmethod
  81. def from_dict(cls, data: Dict[str, Any]) -> "ModelPricing":
  82. """从字典创建"""
  83. return cls(
  84. model=data["model"],
  85. input_price=data.get("input_price", 0.0),
  86. output_price=data.get("output_price", 0.0),
  87. reasoning_price=data.get("reasoning_price"),
  88. cache_creation_price=data.get("cache_creation_price"),
  89. cache_read_price=data.get("cache_read_price"),
  90. provider=data.get("provider"),
  91. description=data.get("description"),
  92. )
  93. class PricingCalculator:
  94. """
  95. 定价计算器
  96. 从 YAML 配置加载定价表,计算 LLM 调用费用
  97. """
  98. def __init__(self, config_path: Optional[str] = None):
  99. """
  100. 初始化定价计算器
  101. Args:
  102. config_path: 定价配置文件路径,默认查找:
  103. 1. 环境变量 AGENT_PRICING_CONFIG
  104. 2. ./pricing.yaml
  105. 3. ./config/pricing.yaml
  106. 4. 使用内置默认配置
  107. """
  108. self._pricing_map: Dict[str, ModelPricing] = {}
  109. self._patterns: List[ModelPricing] = [] # 带通配符的定价
  110. # 加载配置
  111. config_path = self._resolve_config_path(config_path)
  112. if config_path and Path(config_path).exists():
  113. self._load_from_file(config_path)
  114. else:
  115. self._load_defaults()
  116. def _resolve_config_path(self, config_path: Optional[str]) -> Optional[str]:
  117. """解析配置文件路径"""
  118. if config_path:
  119. return config_path
  120. # 检查环境变量
  121. if env_path := os.getenv("AGENT_PRICING_CONFIG"):
  122. return env_path
  123. # 获取 agent 包的根目录(agent/llm/pricing.py -> agent/)
  124. agent_dir = Path(__file__).parent.parent
  125. project_root = agent_dir.parent # 项目根目录
  126. # 检查默认位置(按优先级)
  127. search_paths = [
  128. # 1. 当前工作目录
  129. Path("pricing.yaml"),
  130. Path("config/pricing.yaml"),
  131. # 2. 项目根目录
  132. project_root / "pricing.yaml",
  133. project_root / "config" / "pricing.yaml",
  134. # 3. agent 包目录
  135. agent_dir / "pricing.yaml",
  136. agent_dir / "config" / "pricing.yaml",
  137. ]
  138. for path in search_paths:
  139. if path.exists():
  140. print(f"[Pricing] Loaded config from: {path}")
  141. return str(path)
  142. return None
  143. def _load_from_file(self, config_path: str) -> None:
  144. """从 YAML 文件加载配置"""
  145. with open(config_path, "r", encoding="utf-8") as f:
  146. config = yaml.safe_load(f)
  147. for item in config.get("models", []):
  148. pricing = ModelPricing.from_dict(item)
  149. if "*" in pricing.model:
  150. self._patterns.append(pricing)
  151. else:
  152. self._pricing_map[pricing.model.lower()] = pricing
  153. def _load_defaults(self) -> None:
  154. """加载内置默认定价"""
  155. defaults = self._get_default_pricing()
  156. for item in defaults:
  157. pricing = ModelPricing.from_dict(item)
  158. if "*" in pricing.model:
  159. self._patterns.append(pricing)
  160. else:
  161. self._pricing_map[pricing.model.lower()] = pricing
  162. def _get_default_pricing(self) -> List[Dict[str, Any]]:
  163. """
  164. 内置默认定价表
  165. 价格来源:各提供商官网(2024-12 更新)
  166. 单位:美元 / 1M tokens
  167. """
  168. return [
  169. # ===== OpenAI =====
  170. {"model": "gpt-4o", "input_price": 2.50, "output_price": 10.00, "provider": "openai"},
  171. {"model": "gpt-4o-mini", "input_price": 0.15, "output_price": 0.60, "provider": "openai"},
  172. {"model": "gpt-4-turbo", "input_price": 10.00, "output_price": 30.00, "provider": "openai"},
  173. {"model": "gpt-4", "input_price": 30.00, "output_price": 60.00, "provider": "openai"},
  174. {"model": "gpt-3.5-turbo", "input_price": 0.50, "output_price": 1.50, "provider": "openai"},
  175. # o1/o3 系列(reasoning tokens 单独计价)
  176. {"model": "o1", "input_price": 15.00, "output_price": 60.00, "reasoning_price": 60.00, "provider": "openai"},
  177. {"model": "o1-mini", "input_price": 3.00, "output_price": 12.00, "reasoning_price": 12.00, "provider": "openai"},
  178. {"model": "o1-preview", "input_price": 15.00, "output_price": 60.00, "reasoning_price": 60.00, "provider": "openai"},
  179. {"model": "o3-mini", "input_price": 1.10, "output_price": 4.40, "reasoning_price": 4.40, "provider": "openai"},
  180. # ===== Anthropic Claude =====
  181. {"model": "claude-3-5-sonnet-20241022", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
  182. {"model": "claude-3-5-haiku-20241022", "input_price": 0.80, "output_price": 4.00, "provider": "anthropic"},
  183. {"model": "claude-3-opus-20240229", "input_price": 15.00, "output_price": 75.00, "provider": "anthropic"},
  184. {"model": "claude-3-sonnet-20240229", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
  185. {"model": "claude-3-haiku-20240307", "input_price": 0.25, "output_price": 1.25, "provider": "anthropic"},
  186. # Claude 通配符
  187. {"model": "claude-3-5-sonnet*", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
  188. {"model": "claude-3-opus*", "input_price": 15.00, "output_price": 75.00, "provider": "anthropic"},
  189. {"model": "claude-sonnet-4*", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
  190. {"model": "claude-opus-4*", "input_price": 15.00, "output_price": 75.00, "provider": "anthropic"},
  191. # ===== Google Gemini =====
  192. {"model": "gemini-2.0-flash", "input_price": 0.10, "output_price": 0.40, "provider": "google"},
  193. {"model": "gemini-2.0-flash-thinking", "input_price": 0.10, "output_price": 0.40, "reasoning_price": 0.40, "provider": "google"},
  194. {"model": "gemini-1.5-pro", "input_price": 1.25, "output_price": 5.00, "provider": "google"},
  195. {"model": "gemini-1.5-flash", "input_price": 0.075, "output_price": 0.30, "provider": "google"},
  196. {"model": "gemini-2.5-pro", "input_price": 1.25, "output_price": 10.00, "reasoning_price": 10.00, "provider": "google"},
  197. # Gemini 通配符
  198. {"model": "gemini-2.0*", "input_price": 0.10, "output_price": 0.40, "provider": "google"},
  199. {"model": "gemini-1.5*", "input_price": 1.25, "output_price": 5.00, "provider": "google"},
  200. {"model": "gemini-2.5*", "input_price": 1.25, "output_price": 10.00, "provider": "google"},
  201. # ===== DeepSeek =====
  202. {"model": "deepseek-chat", "input_price": 0.14, "output_price": 0.28, "provider": "deepseek"},
  203. {"model": "deepseek-reasoner", "input_price": 0.55, "output_price": 2.19, "reasoning_price": 2.19, "provider": "deepseek"},
  204. {"model": "deepseek-r1*", "input_price": 0.55, "output_price": 2.19, "reasoning_price": 2.19, "provider": "deepseek"},
  205. # ===== OpenRouter 转发(使用原模型价格)=====
  206. {"model": "anthropic/claude-3-5-sonnet", "input_price": 3.00, "output_price": 15.00, "provider": "openrouter"},
  207. {"model": "anthropic/claude-3-opus", "input_price": 15.00, "output_price": 75.00, "provider": "openrouter"},
  208. {"model": "anthropic/claude-sonnet-4*", "input_price": 3.00, "output_price": 15.00, "provider": "openrouter"},
  209. {"model": "anthropic/claude-opus-4*", "input_price": 15.00, "output_price": 75.00, "provider": "openrouter"},
  210. {"model": "openai/gpt-4o", "input_price": 2.50, "output_price": 10.00, "provider": "openrouter"},
  211. {"model": "openai/o1*", "input_price": 15.00, "output_price": 60.00, "reasoning_price": 60.00, "provider": "openrouter"},
  212. {"model": "google/gemini*", "input_price": 1.25, "output_price": 5.00, "provider": "openrouter"},
  213. {"model": "deepseek/deepseek-r1*", "input_price": 0.55, "output_price": 2.19, "reasoning_price": 2.19, "provider": "openrouter"},
  214. # ===== Yescode 代理 =====
  215. {"model": "claude-sonnet-4.5", "input_price": 3.00, "output_price": 15.00, "cache_creation_price": 3.75, "cache_read_price": 0.30, "provider": "yescode"},
  216. ]
  217. def get_pricing(self, model: str) -> Optional[ModelPricing]:
  218. """
  219. 获取模型定价
  220. Args:
  221. model: 模型名称
  222. Returns:
  223. ModelPricing 或 None(未找到)
  224. """
  225. model_lower = model.lower()
  226. # 精确匹配
  227. if model_lower in self._pricing_map:
  228. return self._pricing_map[model_lower]
  229. # 通配符匹配
  230. for pattern in self._patterns:
  231. if pattern.matches(model):
  232. return pattern
  233. return None
  234. def calculate_cost(
  235. self,
  236. model: str,
  237. usage: TokenUsage,
  238. fallback_input_price: float = 1.0,
  239. fallback_output_price: float = 2.0
  240. ) -> float:
  241. """
  242. 计算费用
  243. Args:
  244. model: 模型名称
  245. usage: Token 使用量
  246. fallback_input_price: 未找到定价时的默认输入价格
  247. fallback_output_price: 未找到定价时的默认输出价格
  248. Returns:
  249. 费用(美元)
  250. """
  251. pricing = self.get_pricing(model)
  252. if pricing:
  253. return pricing.calculate_cost(usage)
  254. # 使用 fallback 价格
  255. fallback = ModelPricing(
  256. model=model,
  257. input_price=fallback_input_price,
  258. output_price=fallback_output_price
  259. )
  260. return fallback.calculate_cost(usage)
  261. def add_pricing(self, pricing: ModelPricing) -> None:
  262. """动态添加定价"""
  263. if "*" in pricing.model:
  264. self._patterns.append(pricing)
  265. else:
  266. self._pricing_map[pricing.model.lower()] = pricing
  267. def list_models(self) -> List[str]:
  268. """列出所有已配置的模型"""
  269. models = list(self._pricing_map.keys())
  270. models.extend(p.model for p in self._patterns)
  271. return sorted(models)
  272. # 全局单例
  273. _calculator: Optional[PricingCalculator] = None
  274. def get_pricing_calculator() -> PricingCalculator:
  275. """获取全局定价计算器"""
  276. global _calculator
  277. if _calculator is None:
  278. _calculator = PricingCalculator()
  279. return _calculator
  280. def calculate_cost(model: str, usage: TokenUsage) -> float:
  281. """
  282. 便捷函数:计算费用
  283. Args:
  284. model: 模型名称
  285. usage: Token 使用量
  286. Returns:
  287. 费用(美元)
  288. """
  289. return get_pricing_calculator().calculate_cost(model, usage)