| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- """
- LLM 定价计算器
- 使用策略模式,支持:
- 1. YAML 配置文件定义模型价格
- 2. 不同 token 类型的差异化定价(input/output/reasoning/cache)
- 3. 自动匹配模型(支持通配符)
- 4. 费用计算
- 定价单位:美元 / 1M tokens
- """
- import os
- import re
- from dataclasses import dataclass, field
- from pathlib import Path
- from typing import Dict, Any, Optional, List
- import yaml
- from .usage import TokenUsage
- @dataclass
- class ModelPricing:
- """
- 单个模型的定价配置
- 所有价格单位:美元 / 1M tokens
- """
- model: str # 模型名称(支持通配符 *)
- input_price: float = 0.0 # 输入 token 价格
- output_price: float = 0.0 # 输出 token 价格
- # 可选的差异化定价
- reasoning_price: Optional[float] = None # 推理 token 价格(默认 = output_price)
- cache_creation_price: Optional[float] = None # 缓存创建价格(默认 = input_price * 1.25)
- cache_read_price: Optional[float] = None # 缓存读取价格(默认 = input_price * 0.1)
- # 元数据
- provider: Optional[str] = None # 提供商
- description: Optional[str] = None # 描述
- def get_reasoning_price(self) -> float:
- """获取推理 token 价格"""
- return self.reasoning_price if self.reasoning_price is not None else self.output_price
- def get_cache_creation_price(self) -> float:
- """获取缓存创建价格"""
- return self.cache_creation_price if self.cache_creation_price is not None else self.input_price * 1.25
- def get_cache_read_price(self) -> float:
- """获取缓存读取价格"""
- return self.cache_read_price if self.cache_read_price is not None else self.input_price * 0.1
- def calculate_cost(self, usage: TokenUsage) -> float:
- """
- 计算费用
- Args:
- usage: Token 使用量
- Returns:
- 费用(美元)
- """
- cost = 0.0
- # 基础输入费用
- # 如果有缓存,需要分开计算
- if usage.cache_read_tokens or usage.cache_creation_tokens:
- # 普通输入 = 总输入 - 缓存读取(缓存读取部分单独计价)
- regular_input = usage.input_tokens - usage.cache_read_tokens
- cost += (regular_input / 1_000_000) * self.input_price
- cost += (usage.cache_read_tokens / 1_000_000) * self.get_cache_read_price()
- cost += (usage.cache_creation_tokens / 1_000_000) * self.get_cache_creation_price()
- else:
- cost += (usage.input_tokens / 1_000_000) * self.input_price
- # 输出费用
- # 如果有 reasoning tokens,需要分开计算
- if usage.reasoning_tokens:
- # 普通输出 = 总输出 - reasoning(reasoning 部分单独计价)
- regular_output = usage.output_tokens - usage.reasoning_tokens
- cost += (regular_output / 1_000_000) * self.output_price
- cost += (usage.reasoning_tokens / 1_000_000) * self.get_reasoning_price()
- else:
- cost += (usage.output_tokens / 1_000_000) * self.output_price
- return cost
- def matches(self, model_name: str) -> bool:
- """
- 检查模型名称是否匹配
- 支持通配符:
- - "gpt-4*" 匹配 "gpt-4", "gpt-4-turbo", "gpt-4o" 等
- - "claude-3-*" 匹配 "claude-3-opus", "claude-3-sonnet" 等
- """
- pattern = self.model.replace("*", ".*")
- return bool(re.match(f"^{pattern}$", model_name, re.IGNORECASE))
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> "ModelPricing":
- """从字典创建"""
- return cls(
- model=data["model"],
- input_price=data.get("input_price", 0.0),
- output_price=data.get("output_price", 0.0),
- reasoning_price=data.get("reasoning_price"),
- cache_creation_price=data.get("cache_creation_price"),
- cache_read_price=data.get("cache_read_price"),
- provider=data.get("provider"),
- description=data.get("description"),
- )
- class PricingCalculator:
- """
- 定价计算器
- 从 YAML 配置加载定价表,计算 LLM 调用费用
- """
- def __init__(self, config_path: Optional[str] = None):
- """
- 初始化定价计算器
- Args:
- config_path: 定价配置文件路径,默认查找:
- 1. 环境变量 AGENT_PRICING_CONFIG
- 2. ./pricing.yaml
- 3. ./config/pricing.yaml
- 4. 使用内置默认配置
- """
- self._pricing_map: Dict[str, ModelPricing] = {}
- self._patterns: List[ModelPricing] = [] # 带通配符的定价
- # 加载配置
- config_path = self._resolve_config_path(config_path)
- if config_path and Path(config_path).exists():
- self._load_from_file(config_path)
- else:
- self._load_defaults()
- def _resolve_config_path(self, config_path: Optional[str]) -> Optional[str]:
- """解析配置文件路径"""
- if config_path:
- return config_path
- # 检查环境变量
- if env_path := os.getenv("AGENT_PRICING_CONFIG"):
- return env_path
- # 获取 agent 包的根目录(agent/llm/pricing.py -> agent/)
- agent_dir = Path(__file__).parent.parent
- project_root = agent_dir.parent # 项目根目录
- # 检查默认位置(按优先级)
- search_paths = [
- # 1. 当前工作目录
- Path("pricing.yaml"),
- Path("config/pricing.yaml"),
- # 2. 项目根目录
- project_root / "pricing.yaml",
- project_root / "config" / "pricing.yaml",
- # 3. agent 包目录
- agent_dir / "pricing.yaml",
- agent_dir / "config" / "pricing.yaml",
- ]
- for path in search_paths:
- if path.exists():
- print(f"[Pricing] Loaded config from: {path}")
- return str(path)
- return None
- def _load_from_file(self, config_path: str) -> None:
- """从 YAML 文件加载配置"""
- with open(config_path, "r", encoding="utf-8") as f:
- config = yaml.safe_load(f)
- for item in config.get("models", []):
- pricing = ModelPricing.from_dict(item)
- if "*" in pricing.model:
- self._patterns.append(pricing)
- else:
- self._pricing_map[pricing.model.lower()] = pricing
- def _load_defaults(self) -> None:
- """加载内置默认定价"""
- defaults = self._get_default_pricing()
- for item in defaults:
- pricing = ModelPricing.from_dict(item)
- if "*" in pricing.model:
- self._patterns.append(pricing)
- else:
- self._pricing_map[pricing.model.lower()] = pricing
- def _get_default_pricing(self) -> List[Dict[str, Any]]:
- """
- 内置默认定价表
- 价格来源:各提供商官网(2024-12 更新)
- 单位:美元 / 1M tokens
- """
- return [
- # ===== OpenAI =====
- {"model": "gpt-4o", "input_price": 2.50, "output_price": 10.00, "provider": "openai"},
- {"model": "gpt-4o-mini", "input_price": 0.15, "output_price": 0.60, "provider": "openai"},
- {"model": "gpt-4-turbo", "input_price": 10.00, "output_price": 30.00, "provider": "openai"},
- {"model": "gpt-4", "input_price": 30.00, "output_price": 60.00, "provider": "openai"},
- {"model": "gpt-3.5-turbo", "input_price": 0.50, "output_price": 1.50, "provider": "openai"},
- # o1/o3 系列(reasoning tokens 单独计价)
- {"model": "o1", "input_price": 15.00, "output_price": 60.00, "reasoning_price": 60.00, "provider": "openai"},
- {"model": "o1-mini", "input_price": 3.00, "output_price": 12.00, "reasoning_price": 12.00, "provider": "openai"},
- {"model": "o1-preview", "input_price": 15.00, "output_price": 60.00, "reasoning_price": 60.00, "provider": "openai"},
- {"model": "o3-mini", "input_price": 1.10, "output_price": 4.40, "reasoning_price": 4.40, "provider": "openai"},
- # ===== Anthropic Claude =====
- {"model": "claude-3-5-sonnet-20241022", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
- {"model": "claude-3-5-haiku-20241022", "input_price": 0.80, "output_price": 4.00, "provider": "anthropic"},
- {"model": "claude-3-opus-20240229", "input_price": 15.00, "output_price": 75.00, "provider": "anthropic"},
- {"model": "claude-3-sonnet-20240229", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
- {"model": "claude-3-haiku-20240307", "input_price": 0.25, "output_price": 1.25, "provider": "anthropic"},
- # Claude 通配符
- {"model": "claude-3-5-sonnet*", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
- {"model": "claude-3-opus*", "input_price": 15.00, "output_price": 75.00, "provider": "anthropic"},
- {"model": "claude-sonnet-4*", "input_price": 3.00, "output_price": 15.00, "provider": "anthropic"},
- {"model": "claude-opus-4*", "input_price": 15.00, "output_price": 75.00, "provider": "anthropic"},
- # ===== Google Gemini =====
- {"model": "gemini-2.0-flash", "input_price": 0.10, "output_price": 0.40, "provider": "google"},
- {"model": "gemini-2.0-flash-thinking", "input_price": 0.10, "output_price": 0.40, "reasoning_price": 0.40, "provider": "google"},
- {"model": "gemini-1.5-pro", "input_price": 1.25, "output_price": 5.00, "provider": "google"},
- {"model": "gemini-1.5-flash", "input_price": 0.075, "output_price": 0.30, "provider": "google"},
- {"model": "gemini-2.5-pro", "input_price": 1.25, "output_price": 10.00, "reasoning_price": 10.00, "provider": "google"},
- # Gemini 通配符
- {"model": "gemini-2.0*", "input_price": 0.10, "output_price": 0.40, "provider": "google"},
- {"model": "gemini-1.5*", "input_price": 1.25, "output_price": 5.00, "provider": "google"},
- {"model": "gemini-2.5*", "input_price": 1.25, "output_price": 10.00, "provider": "google"},
- # ===== DeepSeek =====
- {"model": "deepseek-chat", "input_price": 0.14, "output_price": 0.28, "provider": "deepseek"},
- {"model": "deepseek-reasoner", "input_price": 0.55, "output_price": 2.19, "reasoning_price": 2.19, "provider": "deepseek"},
- {"model": "deepseek-r1*", "input_price": 0.55, "output_price": 2.19, "reasoning_price": 2.19, "provider": "deepseek"},
- # ===== OpenRouter 转发(使用原模型价格)=====
- {"model": "anthropic/claude-3-5-sonnet", "input_price": 3.00, "output_price": 15.00, "provider": "openrouter"},
- {"model": "anthropic/claude-3-opus", "input_price": 15.00, "output_price": 75.00, "provider": "openrouter"},
- {"model": "anthropic/claude-sonnet-4*", "input_price": 3.00, "output_price": 15.00, "provider": "openrouter"},
- {"model": "anthropic/claude-opus-4*", "input_price": 15.00, "output_price": 75.00, "provider": "openrouter"},
- {"model": "openai/gpt-4o", "input_price": 2.50, "output_price": 10.00, "provider": "openrouter"},
- {"model": "openai/o1*", "input_price": 15.00, "output_price": 60.00, "reasoning_price": 60.00, "provider": "openrouter"},
- {"model": "google/gemini*", "input_price": 1.25, "output_price": 5.00, "provider": "openrouter"},
- {"model": "deepseek/deepseek-r1*", "input_price": 0.55, "output_price": 2.19, "reasoning_price": 2.19, "provider": "openrouter"},
- # ===== Yescode 代理 =====
- {"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"},
- ]
- def get_pricing(self, model: str) -> Optional[ModelPricing]:
- """
- 获取模型定价
- Args:
- model: 模型名称
- Returns:
- ModelPricing 或 None(未找到)
- """
- model_lower = model.lower()
- # 精确匹配
- if model_lower in self._pricing_map:
- return self._pricing_map[model_lower]
- # 通配符匹配
- for pattern in self._patterns:
- if pattern.matches(model):
- return pattern
- return None
- def calculate_cost(
- self,
- model: str,
- usage: TokenUsage,
- fallback_input_price: float = 1.0,
- fallback_output_price: float = 2.0
- ) -> float:
- """
- 计算费用
- Args:
- model: 模型名称
- usage: Token 使用量
- fallback_input_price: 未找到定价时的默认输入价格
- fallback_output_price: 未找到定价时的默认输出价格
- Returns:
- 费用(美元)
- """
- pricing = self.get_pricing(model)
- if pricing:
- return pricing.calculate_cost(usage)
- # 使用 fallback 价格
- fallback = ModelPricing(
- model=model,
- input_price=fallback_input_price,
- output_price=fallback_output_price
- )
- return fallback.calculate_cost(usage)
- def add_pricing(self, pricing: ModelPricing) -> None:
- """动态添加定价"""
- if "*" in pricing.model:
- self._patterns.append(pricing)
- else:
- self._pricing_map[pricing.model.lower()] = pricing
- def list_models(self) -> List[str]:
- """列出所有已配置的模型"""
- models = list(self._pricing_map.keys())
- models.extend(p.model for p in self._patterns)
- return sorted(models)
- # 全局单例
- _calculator: Optional[PricingCalculator] = None
- def get_pricing_calculator() -> PricingCalculator:
- """获取全局定价计算器"""
- global _calculator
- if _calculator is None:
- _calculator = PricingCalculator()
- return _calculator
- def calculate_cost(model: str, usage: TokenUsage) -> float:
- """
- 便捷函数:计算费用
- Args:
- model: 模型名称
- usage: Token 使用量
- Returns:
- 费用(美元)
- """
- return get_pricing_calculator().calculate_cost(model, usage)
|