"""LLM 调用成本计算(纯逻辑,无 DB):按 model 分桶累加 token × 单价,返回总成本(元)+ 价格快照。 用量取自 comparison_record.llm_calls[].usage(pricebot 已归一为 prompt/completion_tokens); error / 无 usage 的调用跳过。price_cfg = {per_model:{model:{input_per_1m,output_per_1m}}, default:{...}}。 成本单位「元」——单次亚分级,用 float(不用 *_cents);snapshot 只含本次用到的模型的价(审计用, 不存整张价表)。用到但没配价(既无 per_model 又无 default)的模型 → 快照标 unpriced,成本按 0 计。 """ from __future__ import annotations _PRICE_KEY = "llm_token_price" def get_llm_prices(db) -> dict: """读 LLM 单价配置(app_config;表内无则回退 CONFIG_DEFS 默认)。返回 compute_llm_cost 的 price_cfg。""" from app.repositories import app_config # 延迟 import:compute_llm_cost 纯逻辑不牵连 DB 层 return app_config.get_value(db, _PRICE_KEY) def compute_llm_cost(calls: list[dict], price_cfg: dict) -> tuple[float | None, dict | None]: """遍历 calls 按 model 分桶,cost = Σ(入/1e6*入价 + 出/1e6*出价);无有效调用 → (None, None)。""" if not calls: return None, None per_model = price_cfg.get("per_model") or {} default = price_cfg.get("default") buckets: dict[str, list[int]] = {} # model -> [Σprompt_tokens, Σcompletion_tokens] for c in calls: if c.get("error"): continue usage = c.get("usage") or {} model = c.get("model") or "unknown" b = buckets.setdefault(model, [0, 0]) b[0] += usage.get("prompt_tokens") or 0 b[1] += usage.get("completion_tokens") or 0 if not buckets: # 全是 error / 无 usage return None, None total = 0.0 prices: dict[str, dict] = {} for model, (tin, tout) in buckets.items(): price = per_model.get(model, default) in_p = price.get("input_per_1m") if isinstance(price, dict) else None out_p = price.get("output_per_1m") if isinstance(price, dict) else None # 没配价 / 无 default / 单价残缺或非法(配置页手改 JSON 可能存出脏数据)→ 标记待补价、 # 不计入成本;绝不抛异常,以免连累同一回填里的 token/llm_calls 落库。 if not isinstance(in_p, (int, float)) or not isinstance(out_p, (int, float)): prices[model] = {"input_per_1m": in_p, "output_per_1m": out_p, "unpriced": True} continue total += tin / 1e6 * in_p + tout / 1e6 * out_p prices[model] = { "input_per_1m": in_p, "output_per_1m": out_p, "_source": "per_model" if model in per_model else "default", } return round(total, 6), {"mode": "per_model", "prices": prices}