逐任务真实成本:上游用量事件 + 价目表计价

此前每任务只记一条 estimated(字符数/4,金额为空),注释称
host 协议不暴露真实 token 数。本轮实测推翻了该前提:
- account/usage/read 在中继上是死的(要求 ChatGPT 账号登录),
  不是来源,已在 cost.py docstring 写明死胡同
- app-server 每上游补全发 thread/tokenUsage/updated,last 是该次
  补全的用量;中继 5 调用回合实测 last 之和等于最后 total
  (input/output/cached/reasoning 四路都对得上)

实现:host.py run_turn 从事件窗口按 threadId+turnId 汇总用量,
重试/goal 各轮累加,dispatch/run JSON 与 goal 用量行透传;
sandbox 三路解析(dispatch JSON 解析改用完整 stdout,顺带修
了长 text 下 failed 被误判 completed 的潜 bug);worker 每任务
记一条 actual(重试/fallback 用量累加进同一条),无用量时回落
estimated;cost.py 价目表(默认 OpenAI 挂牌价,env 可覆盖,
无价模型 usd 记 None、报表进 unpriced_actual_tokens 而非 0 美元),
cost_records 加 cached_input_tokens 列(SQLite/PG 两支迁移);
报表与按天行新增 cached_input_tokens/unpriced_actual_tokens。
测试:host 32、cloud 483 全过。
This commit is contained in:
host committed 2026-10-26 18:41:27 +08:00
1 parent 353d6c1aa8
commit 63823fc2e3
11 files changed
+709 -67

No files matched your search

+24 -4
View File
@@ -374,7 +374,7 @@ psycopg,写明“需安装 psycopg”),绝不静默回落到 SQLite,
| `POST /tasks/{id}/heartbeat` | 该项目 `create_task` | 更新 `last_heartbeat = now`;状态为 `queued` 则改为 `running`;返回任务 |
| `POST /admin/sweep` | 认证即可 | 调 `sweep_stale_tasks(db_path, threshold_seconds=300)`(body 可传 `threshold_seconds` 覆盖默认值);把 `running` 且 `last_heartbeat`(无心跳则用 `updated_at`)早于 `now - threshold` 的任务改为 `interrupted` 并记审计 `sweep_interrupted`;返回 `{"swept": [ids]}` |
| `POST /admin/backup` | maintainer 以上 | Postgres 时调 `backup_postgres`、SQLite 时调 `backup_db`(body 可传 `out_dir`,否则用 `CLOUD_BACKUP_DIR` 或默认 `cloud-backups`);返回 `{backend, path, size_bytes, task_count}`,绝不返回数据库密码或备份文件内容 |
| `POST /tasks/{id}/cost` | 该任务所属项目 `read` | 成本聚合:`{task_id, project_id, total_tokens, usd_micros, turns, fallback_count, actual_tokens, estimated_tokens}`;真数与估算分开;不存在 `404`,越权 `403` |
| `GET /tasks/{id}/cost` | 该任务所属项目 `read` | 成本聚合:`{task_id, project_id, total_tokens, usd_micros, turns, fallback_count, actual_tokens, estimated_tokens, cached_input_tokens, unpriced_actual_tokens}`;真数与估算分开,无价 actual 行进 `unpriced_actual_tokens`;不存在 `404`,越权 `403` |
| `POST /projects/{id}/memory` | 该项目 `create_task` | 写线程摘要 body `{thread_id?, goal, conclusion, files_changed?, open_issues?}`,返回 `201 {id}`,并记审计 `create_memory` |
| `GET /projects/{id}/memory?top_k=5` | 该项目 `read` | 最近 active 摘要(默认 top_k=5)+ `prompt_context`(只含 top_k,不全量注入);不存在 `404`,越权 `403` |
| `POST /projects/{id}/memory/{mid}/supersede` | 该项目 `create_task` | 把摘要标废弃(deprecated,不是物理删除),并记审计 `supersede_memory`;摘要不属于该项目则 `404` |
@@ -697,9 +697,29 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
照样跑)。第二次的结果就是最终结果:成功则任务 completed,
`actual_model` 写成 fallback 后的模型,成本记录 `fallback_count=1`;
第二次也失败则任务 failed,`error_text` 同时写清主模型失败原因与
fallback 模型失败原因。边界:真实 token 成本仍是估算(host 协议
不暴露真实 token 数),fallback 不改变成本来源,只改变实际跑的
模型与 `actual_model`/`fallback_count` 的记录。
fallback 模型失败原因。成本来源与 fallback 无关(见下文“真实成本”)。
- 真实成本(逐任务 actual):host.py 在每回合结束时从 app-server
的 `thread/tokenUsage/updated` 事件里汇总上游真实用量(每个上游
补全发一次,`last` 是该次补全的用量,同一 turn 内累加;中继实测
五次 `last` 之和等于最后一次的 `total`,input/output/cached/
reasoning 四路都对得上)。host.py dispatch/run 打印的 JSON 带
`usage`,goal 循环在结论行后另打一行 `usage input=… cached=…
output=… total=… completions=…`;执行器解析后 worker 每任务记
一条 `source="actual"`(重试/fallback 各次尝试的用量都累加进
同一条,不按尝试次数重复记行)。没带回用量时(老版本 server、
回合未产生补全)回落字符估算记 `estimated`,两者在报表里分开。
金额 = 实测 token × 价目表:默认是 OpenAI 挂牌价(gpt-6.1-sol
$2.00/$0.10/$10.00、gpt-6-sol $2.00/$0.20/$10.00,单位美元/百万
token,依次为输入/缓存输入/输出;缓存是输入的子集,不另加;
推理 token 已含在输出里,不重复算),中继的实际账单可能不同,
可用 `CLOUD_MODEL_PRICES_JSON` 或 `CLOUD_MODEL_PRICES_FILE`
按模型覆盖;无价模型记 `usd_micros=None`,报表里进
`unpriced_actual_tokens` 而不是 0 美元。`GET /tasks/{id}/cost` 与
`GET /projects/{id}/cost` 的聚合新增 `cached_input_tokens` 与
`unpriced_actual_tokens`;按天行同样新增这两列。注意死胡同:
app-server 的 `account/usage/read` 在中继上不可用(要求 ChatGPT
账号登录,中继 API key 调它报 `codex account authentication
required`),账户级用量不是本功能的来源,不要重走这条路。
- 容器清理兜底:`DockerExecutor.list_containers(prefix="codex-")`(
`docker ps -a --format {{.Names}}`,失败返回 `[]`)与
`cleanup_stale_containers()`(列出并逐个 kill+rm,单个失败只跳过,