""" Token 用量追踪器(v0.3 新增) 进程内全局单例,按 stage 维度统计 LLM token 消耗。 - LLMClient 自动 record 每次 API call 的 prompt/completion tokens - 调用方在 stage 切换时 set_stage("step1_ontology") - 通过 GET /api/usage/summary 查询 - 用于成本估算和销售定价 说明: 1. 这是 Flask 后端进程内的统计,不包含 OASIS 模拟子进程的 LLM 消耗 (camel-ai 用自己的 LLM client,需另外估算) 2. 进程重启会丢失数据,如需持久化可用 reset() 前先 dump 到文件 3. 价格为每 1K tokens 的 CNY 价格,参考各 provider 官网 (2026-04 价格) """ import threading from typing import Optional, Dict, Any _lock = threading.Lock() # 全局状态:{stage: {model: {prompt_tokens, completion_tokens, calls}}} _stage_usage: Dict[str, Dict[str, Dict[str, int]]] = {} _current_stage: Optional[str] = None # 模型定价表(CNY per 1K tokens, prompt / completion) # 来源:各 provider 2026-04 官网价格 PRICING = { # 智谱 GLM 系列 "glm-4-flash": {"prompt": 0.0001, "completion": 0.0001}, "glm-4-flashx": {"prompt": 0.001, "completion": 0.001}, "glm-4-air": {"prompt": 0.001, "completion": 0.001}, "glm-4-plus": {"prompt": 0.05, "completion": 0.05}, "glm-4.6": {"prompt": 0.05, "completion": 0.05}, "glm-5.1": {"prompt": 0.1, "completion": 0.1}, # SiliconFlow 免费 "qwen/qwen2.5-32b-instruct": {"prompt": 0.0, "completion": 0.0}, "qwen2.5-32b-instruct": {"prompt": 0.0, "completion": 0.0}, "baai/bge-m3": {"prompt": 0.0, "completion": 0.0}, # MiniMax "minimax-m2.7-highspeed": {"prompt": 0.001, "completion": 0.001}, "minimax-m2.5": {"prompt": 0.001, "completion": 0.002}, # Anthropic / OpenAI(备用) "gpt-4o-mini": {"prompt": 0.0011, "completion": 0.0044}, "gpt-4o": {"prompt": 0.018, "completion": 0.072}, "claude-haiku-4-5": {"prompt": 0.0072, "completion": 0.036}, "claude-sonnet-4-6": {"prompt": 0.022, "completion": 0.108}, # 兜底 "default": {"prompt": 0.001, "completion": 0.002}, } def _get_pricing(model: str) -> Dict[str, float]: if not model: return PRICING["default"] key = model.lower().strip() if key in PRICING: return PRICING[key] # 模糊匹配(去掉版本号) for k in PRICING: if k != "default" and (key.startswith(k) or k.startswith(key)): return PRICING[k] return PRICING["default"] def set_stage(name: Optional[str]) -> None: """设置当前 stage 名称,后续的 LLM 调用会归到这个 stage 下""" global _current_stage with _lock: _current_stage = name def get_stage() -> Optional[str]: return _current_stage def record_usage( model: str, prompt_tokens: int, completion_tokens: int, stage: Optional[str] = None, ) -> None: """记录一次 LLM 调用的 token 消耗""" if not isinstance(prompt_tokens, int) or not isinstance(completion_tokens, int): return s = stage or _current_stage or "unknown" with _lock: stage_bucket = _stage_usage.setdefault(s, {}) model_bucket = stage_bucket.setdefault( model or "unknown", {"prompt_tokens": 0, "completion_tokens": 0, "calls": 0}, ) model_bucket["prompt_tokens"] += prompt_tokens model_bucket["completion_tokens"] += completion_tokens model_bucket["calls"] += 1 def get_summary() -> Dict[str, Any]: """ 返回当前累计的 token 用量与估算成本。 格式: { "stages": { "step1_ontology": { "by_model": {"glm-4-flash": {"prompt_tokens": 1234, "completion_tokens": 567, "calls": 3, "estimated_cost_cny": 0.0002}}, "prompt_tokens": 1234, "completion_tokens": 567, "calls": 3, "estimated_cost_cny": 0.0002 }, ... }, "total": { "prompt_tokens": 50000, "completion_tokens": 12000, "calls": 45, "estimated_cost_cny": 0.0123 } } """ with _lock: stages_out = {} total_prompt = 0 total_completion = 0 total_calls = 0 total_cost = 0.0 for stage_name, models in _stage_usage.items(): stage_data = { "by_model": {}, "prompt_tokens": 0, "completion_tokens": 0, "calls": 0, "estimated_cost_cny": 0.0, } for model_name, m in models.items(): pricing = _get_pricing(model_name) cost = ( m["prompt_tokens"] / 1000.0 * pricing["prompt"] + m["completion_tokens"] / 1000.0 * pricing["completion"] ) stage_data["by_model"][model_name] = { "prompt_tokens": m["prompt_tokens"], "completion_tokens": m["completion_tokens"], "calls": m["calls"], "estimated_cost_cny": round(cost, 6), } stage_data["prompt_tokens"] += m["prompt_tokens"] stage_data["completion_tokens"] += m["completion_tokens"] stage_data["calls"] += m["calls"] stage_data["estimated_cost_cny"] += cost stage_data["estimated_cost_cny"] = round(stage_data["estimated_cost_cny"], 6) stages_out[stage_name] = stage_data total_prompt += stage_data["prompt_tokens"] total_completion += stage_data["completion_tokens"] total_calls += stage_data["calls"] total_cost += stage_data["estimated_cost_cny"] return { "stages": stages_out, "total": { "prompt_tokens": total_prompt, "completion_tokens": total_completion, "calls": total_calls, "estimated_cost_cny": round(total_cost, 6), }, "current_stage": _current_stage, } def reset(stage: Optional[str] = None) -> None: """清空统计。stage=None 全部清空,否则只清空指定 stage""" global _stage_usage with _lock: if stage is None: _stage_usage = {} else: _stage_usage.pop(stage, None) def estimate_simulation_cost( rounds: int, active_agents_per_round_avg: int, llm_calls_per_action: int = 4, avg_prompt_tokens_per_call: int = 800, avg_completion_tokens_per_call: int = 200, model: str = "glm-4-flash", ) -> Dict[str, Any]: """ 估算 OASIS 模拟子进程的 token 消耗(无法精确测,只能估算) OASIS 内部每个 agent action 通常需要 3-5 次 LLM 调用: 1. 读 timeline / 推荐过滤 2. 决策(要做什么动作) 3. 生成内容 4. 偶尔 reflection Args: rounds: 模拟轮数 active_agents_per_round_avg: 每轮平均激活的 agent 数 llm_calls_per_action: 每个 action 内部 LLM 调用次数(默认 4) avg_prompt_tokens_per_call: 平均 prompt token avg_completion_tokens_per_call: 平均 completion token model: 模型名(用于查询定价) Returns: 估算结果含 token / 成本范围(low/mid/high) """ total_actions = rounds * active_agents_per_round_avg total_calls = total_actions * llm_calls_per_action pricing = _get_pricing(model) mid_prompt = total_calls * avg_prompt_tokens_per_call mid_completion = total_calls * avg_completion_tokens_per_call mid_cost = ( mid_prompt / 1000.0 * pricing["prompt"] + mid_completion / 1000.0 * pricing["completion"] ) return { "model": model, "rounds": rounds, "active_agents_per_round_avg": active_agents_per_round_avg, "llm_calls_per_action": llm_calls_per_action, "total_actions": total_actions, "total_llm_calls": total_calls, "estimated_prompt_tokens": mid_prompt, "estimated_completion_tokens": mid_completion, "estimated_cost_cny": { "low": round(mid_cost * 0.6, 4), "mid": round(mid_cost, 4), "high": round(mid_cost * 1.6, 4), }, "note": "这是估算值,OASIS 模拟在子进程中运行,无法精确追踪。实际值可能在 low-high 区间内浮动。", }