Преглед изворни кода

fix: tokens统计、tokens计价

tanjingyu пре 3 недеља
родитељ
комит
31f331b192
3 измењених фајлова са 863 додато и 0 уклоњено
  1. 350 0
      agent/llm/pricing.py
  2. 297 0
      agent/llm/usage.py
  3. 216 0
      config/pricing.yaml

+ 350 - 0
agent/llm/pricing.py

@@ -0,0 +1,350 @@
+"""
+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"},
+        ]
+
+    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)

+ 297 - 0
agent/llm/usage.py

@@ -0,0 +1,297 @@
+"""
+Token Usage 数据模型和费用计算
+
+支持各种 LLM 提供商的完整 token 统计:
+- 基础 tokens: input/output
+- 思考 tokens: reasoning/thinking (OpenAI o1/o3, DeepSeek R1, Gemini 2.x)
+- 缓存 tokens: cache_creation/cache_read (Claude)
+- 其他: cached_content (Gemini)
+
+设计模式:
+- TokenUsage: 不可变数据类,表示单次调用的 token 使用
+- TokenUsageAccumulator: 累加器,用于统计多次调用
+- PricingCalculator: 策略模式,根据定价表计算费用
+"""
+
+from dataclasses import dataclass, field
+from typing import Dict, Any, Optional
+import copy
+
+
+@dataclass(frozen=True)
+class TokenUsage:
+    """
+    Token 使用量(不可变)
+
+    统一所有提供商的 token 统计字段,未使用的字段为 0
+    """
+    # 基础 tokens(所有提供商都有)
+    input_tokens: int = 0           # 输入 tokens (prompt_tokens)
+    output_tokens: int = 0          # 输出 tokens (completion_tokens)
+
+    # 思考/推理 tokens(部分模型)
+    # - OpenAI o1/o3: reasoning_tokens (在 completion_tokens_details 中)
+    # - DeepSeek R1: reasoning_tokens
+    # - Gemini 2.x thinking mode: thoughts_tokens
+    reasoning_tokens: int = 0
+
+    # 缓存相关 tokens(Claude)
+    # - cache_creation_input_tokens: 创建缓存消耗的 tokens
+    # - cache_read_input_tokens: 读取缓存的 tokens(通常更便宜)
+    cache_creation_tokens: int = 0
+    cache_read_tokens: int = 0
+
+    # Gemini 特有
+    cached_content_tokens: int = 0  # cachedContentTokenCount
+
+    @property
+    def total_tokens(self) -> int:
+        """总 tokens(input + output,不含 reasoning)"""
+        return self.input_tokens + self.output_tokens
+
+    @property
+    def total_input_tokens(self) -> int:
+        """
+        总输入 tokens
+
+        对于 Claude 带缓存的情况:
+        实际输入 = input_tokens(已包含 cache_read)
+        计费输入 = input_tokens - cache_read_tokens + cache_creation_tokens
+        """
+        return self.input_tokens
+
+    @property
+    def total_output_tokens(self) -> int:
+        """
+        总输出 tokens
+
+        对于有 reasoning 的模型:
+        output_tokens 通常已包含 reasoning_tokens
+        """
+        return self.output_tokens
+
+    @property
+    def billable_input_tokens(self) -> int:
+        """
+        计费输入 tokens(考虑缓存折扣)
+
+        Claude 缓存定价:
+        - cache_read: 0.1x 价格
+        - cache_creation: 1.25x 价格
+        - 普通 input: 1x 价格
+
+        这里返回等效的全价 tokens 数
+        """
+        # 普通输入 = 总输入 - 缓存读取
+        regular_input = self.input_tokens - self.cache_read_tokens
+        # 等效计费 = 普通输入 + 缓存读取*0.1 + 缓存创建*1.25
+        # 简化:返回原始值,让 PricingCalculator 处理
+        return self.input_tokens
+
+    def __add__(self, other: "TokenUsage") -> "TokenUsage":
+        """支持 + 运算符累加"""
+        if not isinstance(other, TokenUsage):
+            return NotImplemented
+        return TokenUsage(
+            input_tokens=self.input_tokens + other.input_tokens,
+            output_tokens=self.output_tokens + other.output_tokens,
+            reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
+            cache_creation_tokens=self.cache_creation_tokens + other.cache_creation_tokens,
+            cache_read_tokens=self.cache_read_tokens + other.cache_read_tokens,
+            cached_content_tokens=self.cached_content_tokens + other.cached_content_tokens,
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典(只包含非零字段)"""
+        result = {
+            "input_tokens": self.input_tokens,
+            "output_tokens": self.output_tokens,
+            "total_tokens": self.total_tokens,
+        }
+        # 只添加非零的可选字段
+        if self.reasoning_tokens:
+            result["reasoning_tokens"] = self.reasoning_tokens
+        if self.cache_creation_tokens:
+            result["cache_creation_tokens"] = self.cache_creation_tokens
+        if self.cache_read_tokens:
+            result["cache_read_tokens"] = self.cache_read_tokens
+        if self.cached_content_tokens:
+            result["cached_content_tokens"] = self.cached_content_tokens
+        return result
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "TokenUsage":
+        """从字典创建(兼容旧格式)"""
+        return cls(
+            input_tokens=data.get("input_tokens") or data.get("prompt_tokens", 0),
+            output_tokens=data.get("output_tokens") or data.get("completion_tokens", 0),
+            reasoning_tokens=data.get("reasoning_tokens", 0),
+            cache_creation_tokens=data.get("cache_creation_tokens", 0),
+            cache_read_tokens=data.get("cache_read_tokens", 0),
+            cached_content_tokens=data.get("cached_content_tokens", 0),
+        )
+
+    @classmethod
+    def from_openai(cls, usage: Dict[str, Any]) -> "TokenUsage":
+        """
+        从 OpenAI 格式创建
+
+        OpenAI 格式:
+        {
+            "prompt_tokens": 100,
+            "completion_tokens": 50,
+            "total_tokens": 150,
+            "completion_tokens_details": {
+                "reasoning_tokens": 20  # o1/o3 模型
+            }
+        }
+        """
+        reasoning = 0
+        if details := usage.get("completion_tokens_details"):
+            reasoning = details.get("reasoning_tokens", 0)
+
+        return cls(
+            input_tokens=usage.get("prompt_tokens", 0),
+            output_tokens=usage.get("completion_tokens", 0),
+            reasoning_tokens=reasoning,
+        )
+
+    @classmethod
+    def from_anthropic(cls, usage: Dict[str, Any]) -> "TokenUsage":
+        """
+        从 Anthropic/Claude 格式创建
+
+        Claude 格式:
+        {
+            "input_tokens": 100,
+            "output_tokens": 50,
+            "cache_creation_input_tokens": 1000,  # 可选
+            "cache_read_input_tokens": 500        # 可选
+        }
+        """
+        return cls(
+            input_tokens=usage.get("input_tokens", 0),
+            output_tokens=usage.get("output_tokens", 0),
+            cache_creation_tokens=usage.get("cache_creation_input_tokens", 0),
+            cache_read_tokens=usage.get("cache_read_input_tokens", 0),
+        )
+
+    @classmethod
+    def from_gemini(cls, usage_metadata: Dict[str, Any]) -> "TokenUsage":
+        """
+        从 Gemini 格式创建
+
+        Gemini 格式:
+        {
+            "promptTokenCount": 100,
+            "candidatesTokenCount": 50,
+            "totalTokenCount": 150,
+            "cachedContentTokenCount": 0,    # 可选
+            "thoughtsTokenCount": 20          # Gemini 2.x thinking mode
+        }
+        """
+        return cls(
+            input_tokens=usage_metadata.get("promptTokenCount", 0),
+            output_tokens=usage_metadata.get("candidatesTokenCount", 0),
+            reasoning_tokens=usage_metadata.get("thoughtsTokenCount", 0),
+            cached_content_tokens=usage_metadata.get("cachedContentTokenCount", 0),
+        )
+
+    @classmethod
+    def from_deepseek(cls, usage: Dict[str, Any]) -> "TokenUsage":
+        """
+        从 DeepSeek 格式创建
+
+        DeepSeek R1 格式(OpenAI 兼容 + 扩展):
+        {
+            "prompt_tokens": 100,
+            "completion_tokens": 50,
+            "reasoning_tokens": 30,  # DeepSeek R1 特有
+            "total_tokens": 150
+        }
+        """
+        return cls(
+            input_tokens=usage.get("prompt_tokens", 0),
+            output_tokens=usage.get("completion_tokens", 0),
+            reasoning_tokens=usage.get("reasoning_tokens", 0),
+        )
+
+
+class TokenUsageAccumulator:
+    """
+    Token 使用量累加器
+
+    用于在 Trace 级别累计多次 LLM 调用的 token 使用
+    """
+
+    def __init__(self):
+        self._input_tokens: int = 0
+        self._output_tokens: int = 0
+        self._reasoning_tokens: int = 0
+        self._cache_creation_tokens: int = 0
+        self._cache_read_tokens: int = 0
+        self._cached_content_tokens: int = 0
+        self._call_count: int = 0
+
+    def add(self, usage: TokenUsage) -> None:
+        """累加一次调用的 token 使用"""
+        self._input_tokens += usage.input_tokens
+        self._output_tokens += usage.output_tokens
+        self._reasoning_tokens += usage.reasoning_tokens
+        self._cache_creation_tokens += usage.cache_creation_tokens
+        self._cache_read_tokens += usage.cache_read_tokens
+        self._cached_content_tokens += usage.cached_content_tokens
+        self._call_count += 1
+
+    @property
+    def total(self) -> TokenUsage:
+        """获取累计的 TokenUsage"""
+        return TokenUsage(
+            input_tokens=self._input_tokens,
+            output_tokens=self._output_tokens,
+            reasoning_tokens=self._reasoning_tokens,
+            cache_creation_tokens=self._cache_creation_tokens,
+            cache_read_tokens=self._cache_read_tokens,
+            cached_content_tokens=self._cached_content_tokens,
+        )
+
+    @property
+    def call_count(self) -> int:
+        """调用次数"""
+        return self._call_count
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        result = self.total.to_dict()
+        result["call_count"] = self._call_count
+        return result
+
+
+# 向后兼容的别名
+def create_usage_from_response(
+    provider: str,
+    usage_data: Dict[str, Any]
+) -> TokenUsage:
+    """
+    根据提供商创建 TokenUsage
+
+    Args:
+        provider: 提供商名称 ("openai", "anthropic", "gemini", "deepseek", "openrouter")
+        usage_data: API 返回的 usage 数据
+
+    Returns:
+        TokenUsage 实例
+    """
+    provider = provider.lower()
+
+    if provider in ("openai", "openrouter"):
+        return TokenUsage.from_openai(usage_data)
+    elif provider in ("anthropic", "claude"):
+        return TokenUsage.from_anthropic(usage_data)
+    elif provider == "gemini":
+        return TokenUsage.from_gemini(usage_data)
+    elif provider == "deepseek":
+        return TokenUsage.from_deepseek(usage_data)
+    else:
+        # 默认使用 OpenAI 格式
+        return TokenUsage.from_openai(usage_data)

+ 216 - 0
config/pricing.yaml

@@ -0,0 +1,216 @@
+# LLM 定价配置
+#
+# 价格单位:美元 / 1M tokens
+# 支持通配符:* 匹配任意字符
+#
+# 字段说明:
+#   model: 模型名称(必填)
+#   input_price: 输入 token 价格(必填)
+#   output_price: 输出 token 价格(必填)
+#   reasoning_price: 推理 token 价格(可选,默认 = output_price)
+#   cache_creation_price: 缓存创建价格(可选,默认 = input_price * 1.25)
+#   cache_read_price: 缓存读取价格(可选,默认 = input_price * 0.1)
+#   provider: 提供商名称(可选,用于分类)
+#   description: 描述(可选)
+#
+# 使用方法:
+#   1. 复制此文件到项目根目录或 config/ 目录
+#   2. 或设置环境变量 AGENT_PRICING_CONFIG 指向配置文件
+#   3. 根据实际使用的模型修改价格
+
+models:
+  # ===== 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
+
+  # o1 系列(有 reasoning tokens)
+  - model: o1
+    input_price: 15.00
+    output_price: 60.00
+    reasoning_price: 60.00  # reasoning tokens 和 output 同价
+    provider: openai
+
+  - model: o1-mini
+    input_price: 3.00
+    output_price: 12.00
+    reasoning_price: 12.00
+    provider: openai
+
+  - model: o3-mini
+    input_price: 1.10
+    output_price: 4.40
+    reasoning_price: 4.40
+    provider: openai
+
+  # ===== Anthropic Claude =====
+  # Claude 支持 prompt caching,缓存价格:
+  #   - cache_creation: 1.25x input_price
+  #   - cache_read: 0.1x input_price
+  - model: claude-3-5-sonnet-20241022
+    input_price: 3.00
+    output_price: 15.00
+    cache_creation_price: 3.75   # 3.00 * 1.25
+    cache_read_price: 0.30       # 3.00 * 0.1
+    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
+
+  # Claude 通配符(匹配新版本)
+  - model: claude-3-5-sonnet*
+    input_price: 3.00
+    output_price: 15.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.5-pro
+    input_price: 1.25
+    output_price: 10.00
+    reasoning_price: 10.00  # thinking mode
+    provider: google
+
+  - 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
+
+  # Gemini 通配符
+  - model: gemini-2.5*
+    input_price: 1.25
+    output_price: 10.00
+    provider: google
+
+  - model: gemini-2.0*
+    input_price: 0.10
+    output_price: 0.40
+    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 转发 =====
+  # OpenRouter 使用 provider/model 格式
+  - model: anthropic/claude-sonnet-4.5
+    input_price: 3.00
+    output_price: 15.00
+    provider: openrouter
+
+  - model: anthropic/claude-opus-4.5
+    input_price: 5.00
+    output_price: 25.00
+    provider: openrouter
+
+  - model: anthropic/claude-opus-4.6
+    input_price: 5.00
+    output_price: 25.00
+    provider: openrouter
+
+  - model: anthropic/claude-haiku-4.5
+    input_price: 1.00
+    output_price: 5.00
+    provider: openrouter
+
+  - model: anthropic/claude-sonnet-4
+    input_price: 3.00
+    output_price: 15.00
+    provider: openrouter
+
+  - model: anthropic/claude*
+    input_price: 3.00
+    output_price: 15.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-3-pro-preview
+    input_price: 2
+    output_price: 12
+    reasoning_price: 12
+    provider: openrouter
+
+  - model: google/gemini-3-flash-preview
+    input_price: 0.50
+    output_price: 3
+    reasoning_price: 3
+    provider: openrouter
+
+  - model: google/gemini*
+    input_price: 0.30
+    output_price: 2.50
+    reasoning_price: 2.50
+    provider: openrouter
+
+  - model: deepseek/deepseek-r1*
+    input_price: 0.55
+    output_price: 2.19
+    reasoning_price: 2.19
+    provider: openrouter