diff --git a/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/newapi_cost.py b/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/newapi_cost.py new file mode 100644 index 00000000..76ff41fb --- /dev/null +++ b/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/newapi_cost.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""new-api 权威成本读取(W4 成本侧① · HJ-NEWAPI-BILL-001 薄片①)。 + +定位(评审 v2 整改后的薄片范围): + 把 W4 的 LLM 成本来源从「客户端 token×假设单价 估算」切到 **new-api `logs.quota` 权威**—— + logs.quota 已含真实模型倍率(new-api 计费引擎算好的),比客户端估算准。只读、零依赖生成路径、 + 零回归风险;且 new-api 历史日志已在库,无需新跑批即可验证。 + +读取路径(评审实测裁决): + admin HTTP API(/api/log)鉴权 finicky 且 127.0.0.1:3000 回环超时(status=000,实测); + **直读 new-api PostgreSQL**(内网、proven、CLAUDE.md 允许内网口令入库)经 `ssh mini-infra docker exec psql`。 + 连接参数为内网常量(可环境变量覆盖),非公网密钥。 + +平台系统 vs 创作者成本归属(薄片②): + 按 logs.token_name 分组——`--platform-token ` 标记平台铺量 token,其余归创作者侧。 + (per-creator 用户/支付/订阅按评审 v2 推迟到支付通道真实化后,本工具不涉。) + +用法: + python3 newapi_cost.py --batch runs/merge-prod-20 # 从批 ledger 推窗口 + 交叉校验 report.json + python3 newapi_cost.py --since '2026-06-10T14:40:00+08:00' --until '2026-06-10T15:10:00+08:00' + python3 newapi_cost.py --minutes 60 # 最近 60 分钟 + 环境变量:NEWAPI_PG_SSH(默认 mini-infra) / NEWAPI_PG_PW / NEWAPI_USD_CNY(默认 7.2) +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys + +# —— new-api 配额→¥ 换算(评审 F3/F4 修正)—— +# QuotaPerUnit 库内无此 key(实查 options 仅 9 键),用 new-api 编译默认:500000 quota = 1 USD。 +QUOTA_PER_UNIT = float(os.environ.get("NEWAPI_QUOTA_PER_UNIT", "500000")) +# USD→¥ 汇率为标注假设(非 new-api 配置项);售卖锁价时须快照,此处仅成本台账折算用。 +USD_CNY = float(os.environ.get("NEWAPI_USD_CNY", "7.2")) + +# —— 直读 PG 连接(内网常量,CLAUDE.md 允许;可环境变量覆盖)—— +PG_SSH = os.environ.get("NEWAPI_PG_SSH", "mini-infra") +PG_CONTAINER = os.environ.get("NEWAPI_PG_CONTAINER", "infra-postgres") +PG_PW = os.environ.get("NEWAPI_PG_PW", "f6710e2d0294eb1c10e26a805a64bc54") +PG_USER = os.environ.get("NEWAPI_PG_USER", "root") +PG_DB = os.environ.get("NEWAPI_PG_DB", "new-api") + + +def _psql(sql: str) -> list[list[str]]: + """经 ssh+docker exec 跑只读 psql,返回 `|` 分隔的行(-tA 无表头无对齐,默认分隔符)。 + + 注:SQL 内禁用双引号(远程经 sh -c "..." 传递,双引号会破坏分层引用);字段分隔用 + psql 默认 `|`(弃 -F'\t' 字面 tab——会让远程 argv 解析挂起,实测)。 + """ + inner = ( + 'docker exec -e PGPASSWORD=%s %s psql -U %s -d %s -tA -c "%s"' + % (PG_PW, PG_CONTAINER, PG_USER, PG_DB, sql) + ) + out = subprocess.run( + ["ssh", PG_SSH, inner], + capture_output=True, text=True, timeout=30, + ) + if out.returncode != 0: + raise RuntimeError("psql 读取失败:%s" % (out.stderr.strip()[:300])) + rows = [] + for line in out.stdout.splitlines(): + line = line.strip() + if line: + rows.append(line.split("|")) + return rows + + +def _iso_to_epoch(iso: str) -> int: + """ISO 时间 → unix 秒(logs.created_at 是 epoch int)。经 PG 自身转,避开本地 tz 依赖。""" + r = _psql("select extract(epoch from timestamptz '%s')::bigint" % iso) + return int(r[0][0]) + + +def _batch_window(run_dir: str) -> tuple[str, str]: + """从批 ledger.jsonl 的首/末 ts 推窗口(含前后各 2 分钟余量覆盖收尾调用)。""" + led = os.path.join(run_dir, "ledger.jsonl") + ts = [] + with open(led, encoding="utf-8") as f: + for line in f: + try: + ts.append(json.loads(line)["ts"]) + except Exception: # noqa: BLE001 - 跳过非法行 + continue + if not ts: + raise RuntimeError("ledger 无可用 ts:%s" % led) + return min(ts), max(ts) + + +def _client_estimate(run_dir: str): + """读批 report.json 的 client-side 估算(tokenUsage),用于交叉校验。缺则 None。""" + rep = os.path.join(run_dir, "report.json") + if not os.path.exists(rep): + return None + try: + with open(rep, encoding="utf-8") as f: + data = json.load(f) + return (data.get("cost") or {}).get("tokenUsage") + except Exception: # noqa: BLE001 + return None + + +def main() -> int: + ap = argparse.ArgumentParser(description="new-api 权威成本读取(W4 成本侧①)") + ap.add_argument("--batch", help="批 run 目录(含 ledger.jsonl/report.json)→ 推窗口+交叉校验") + ap.add_argument("--since", help="ISO 起(与 --until 配对)") + ap.add_argument("--until", help="ISO 止") + ap.add_argument("--minutes", type=float, help="最近 N 分钟") + ap.add_argument("--platform-token", default="", + help="标记为平台铺量的 token_name(其余归创作者侧,薄片②归属)") + args = ap.parse_args() + + client_est = None + if args.batch: + since_iso, until_iso = _batch_window(args.batch) + client_est = _client_estimate(args.batch) + since = _iso_to_epoch(since_iso) - 120 + until = _iso_to_epoch(until_iso) + 120 + window_desc = "批 %s 窗口 %s ~ %s(±120s)" % (args.batch, since_iso, until_iso) + elif args.since and args.until: + since = _iso_to_epoch(args.since) + until = _iso_to_epoch(args.until) + window_desc = "%s ~ %s" % (args.since, args.until) + elif args.minutes: + now = _psql("select extract(epoch from now())::bigint")[0][0] + until = int(now) + since = until - int(args.minutes * 60) + window_desc = "最近 %g 分钟" % args.minutes + else: + ap.error("需 --batch / (--since 且 --until) / --minutes 之一") + return 2 + + # type=2 = 消费日志(new-api:消费日志 type=2);按 model × token_name 聚合权威 quota + sql = ( + "select model_name, coalesce(token_name,'') tn, count(*), " + "coalesce(sum(quota),0), coalesce(sum(prompt_tokens),0), coalesce(sum(completion_tokens),0) " + "from logs where type=2 and created_at between %d and %d " + "group by model_name, tn order by 4 desc" + ) % (since, until) + rows = _psql(sql) + + yuan_per_quota = USD_CNY / QUOTA_PER_UNIT + tot_calls = tot_quota = tot_pt = tot_ct = 0 + plat = {"calls": 0, "quota": 0} + crea = {"calls": 0, "quota": 0} + + print("==== new-api 权威成本(logs.quota,含真实倍率) ====") + print("窗口:%s" % window_desc) + print("换算:QuotaPerUnit=%g/USD(库内无此 key,编译默认)× USD→¥=%g ⇒ ¥%.6g/quota【FX 为假设】" + % (QUOTA_PER_UNIT, USD_CNY, yuan_per_quota)) + print("--- 按 model × token_name ---") + for model, tn, calls, quota, pt, ct in rows: + calls, quota, pt, ct = int(calls), int(quota), int(pt), int(ct) + tot_calls += calls; tot_quota += quota; tot_pt += pt; tot_ct += ct + bucket = plat if (args.platform_token and tn == args.platform_token) else crea + bucket["calls"] += calls; bucket["quota"] += quota + print(" %-18s / %-16s : %4d 次, %8d quota, ¥%.4f" + % (model, tn or "—", calls, quota, quota * yuan_per_quota)) + + print("--- 合计 ---") + print(" %d 次调用, %d quota, %d prompt + %d completion token, **¥%.4f**" + % (tot_calls, tot_quota, tot_pt, tot_ct, tot_quota * yuan_per_quota)) + if tot_calls: + print(" 均 %.0f quota/次 ≈ ¥%.5f/次" % (tot_quota / tot_calls, tot_quota / tot_calls * yuan_per_quota)) + + # 薄片②:平台铺量 vs 创作者成本归属 + if args.platform_token: + print("--- 归属(platform-token=%s)---" % args.platform_token) + print(" 平台铺量 : %d 次, ¥%.4f" % (plat["calls"], plat["quota"] * yuan_per_quota)) + print(" 创作者侧 : %d 次, ¥%.4f" % (crea["calls"], crea["quota"] * yuan_per_quota)) + + # 交叉校验:客户端估算 vs new-api 权威 + if client_est: + print("--- 交叉校验(client-side 估算 vs new-api 权威)---") + ce_cost = client_est.get("estCostYuan") + ce_tok = client_est.get("total_tokens") + print(" client 估算:¥%s(%s token,单价假设 ¥%s/M)" + % (ce_cost, ce_tok, client_est.get("pricePerMTokenYuan"))) + print(" new-api 权威:¥%.4f(%d quota)" % (tot_quota * yuan_per_quota, tot_quota)) + print(" → 权威为准;client 估算保留为 new-api 不可达时的兜底/交叉校验。") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/report.py b/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/report.py index f89aa1ec..9dce73af 100644 --- a/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/report.py +++ b/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/report.py @@ -316,6 +316,7 @@ def _render_markdown(rep): % (round(tu["estCostYuan"] / max(1, rep["cost"].get("acceptCount", 1)), 4))) elif tu is not None: lines.append("- token 计量(W4):本批通道未返 usage(calls_counted=0),无 token 数据——见 llm_client 计量覆盖率说明") + lines.append("- **权威成本(W4 薄片①)**:上为 client-side 估算(单价假设);**new-api `logs.quota` 权威成本**(含真实倍率+生成/裁判分列)运行 `newapi_cost.py --batch `(HJ-NEWAPI-BILL-001 薄片①)") lines.append("") lines.append("## 9. 金丝雀发布清单(≤10/批)") lines.append("") diff --git a/docs/mvp/单位经济敏感性模型.md b/docs/mvp/单位经济敏感性模型.md index f0468746..403a1dd3 100644 --- a/docs/mvp/单位经济敏感性模型.md +++ b/docs/mvp/单位经济敏感性模型.md @@ -44,7 +44,8 @@ 结构:单款成本 = LLM 调用(设计 1 + 对抗 1 + 修复 ≤1 + 裁判 1 ≈ 4±1 次【确证:编排器流水线结构】× 每次 ~3-6K token【推断:max_tokens=4096 上限,实际未计量】)+ 失败摊销 ×1.25【确证:accept 80% 口径】+ 存储(MB 级,可忽略)。 - 若单价 2 元/百万 token【假设·待网关账单】→ **≈ 0.03-0.08 元/款**;单价 ×10 也 <1 元/款。 - **文本生成不是成本瓶颈**。真正未计入、可能咬人的两项:**素材生成 GPU**(ComfyUI 未部署,零数据)与**人审兜底人力**——列为 W4 测量项,本模型不编数。 -- **✅ token 计量埋点已就位(2026-06-11)**:`orchestrator/llm_client.py` 累加全部 chat_json 调用的 usage(prompt/completion/total + calls_counted 覆盖率),批报告「## 8. 成本」自动渲染总 token / 均 token/次 / 估算成本 / 单游戏成本;单价 `PRICE_PER_MTOKEN_YUAN=2.0` 为**假设占位**(环境变量 `NEWAPI_PRICE_PER_MTOKEN` 可覆盖,真实网关账单到位即改此一处自动校准全部估算)。**真实 token 数下次批跑自动采集**(历史批无 usage 数据,不回填)。单测 3 绿(累加/缺 usage 不臆造/成本估算)。⚠ 早期观察:M2.7 推理型含 reasoning_content,每次 token 可能高于本模型「3-6K」推断——以实采为准。 +- **✅ token 计量埋点已就位(2026-06-11)**:`orchestrator/llm_client.py` 累加 chat_json 的 usage,批报告「## 8. 成本」渲染;单价 `PRICE_PER_MTOKEN_YUAN=2.0` 为假设占位。**此为客户端估算,已降为 fallback**(见下)。 +- **✅✅ new-api `logs.quota` 权威成本已接通(2026-06-11,HJ-NEWAPI-BILL-001 薄片①,取代上面的客户端估算为权威源)**:`orchestrator/newapi_cost.py` 直读 new-api PostgreSQL logs(含真实模型倍率,new-api 计费引擎算好),按 model×token_name 聚合、生成/裁判分列、平台铺量 vs 创作者归属、交叉校验客户端估算。**实测权威数(merge-prod-20,20 创意/57 调用/19 accept)= ¥0.59 总、¥0.0103/次、约 ¥0.031/款**(MiniMax-M2.7 53 次 ¥0.58 + deepseek-v4-flash 裁判 4 次 ¥0.005)——**确证文本生成成本可忽略**(与 §3 三线排序结论一致,自有端 IAA 万级 DAU 才回本,近期现金靠 B 端)。换算 `QuotaPerUnit=500000/USD`(new-api 编译默认,库内无此 key)× USD→¥ 假设 7.2。**历史批无需重跑即可读**(new-api logs 已在库)。⚠ 评审整改:per-creator 计量/支付/订阅按 `2026-06-11-newapi计费平面集成-review.md` v2 推迟到支付通道真实化后(admin API 无法替他人铸 token + 支付通道 ICP/进件 3-5 周阻塞)。 ## 6. 口径一致性勘误(随本拍板执行)