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

此前每任务只记一条 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

+39 -14
View File
@@ -26,9 +26,12 @@
4. 写回:server 的 tasks 表没有 result/error 列,所以执行结果
的持久化分三处,各复用已有结构、不另起字段名:
- tasks 表:最终状态 + updated_at(取消时为 interrupted);
- cost.py CostStore:每轮实际执行记一条 source="estimated"
的成本记录(host 协议不暴露真实 token 数,估算绝不冒充
actual;total_tokens 由 CostStore.record 按 input+output 算);
- cost.py CostStore:每轮实际执行记一条成本记录;执行器带回
上游真实用量(host.py 从 tokenUsage/updated 事件汇总)时记
source="actual"(input/output/cached 全是实测值,usd 按价目
表算、无价模型记 None);没带回用量时回落 cost.py 估算记
estimated,估算绝不冒充 actual;total_tokens 由
CostStore.record 按 input+output 算;
- models.py:可 fallback 的失败(404/429/5xx/timeout,401/400
不算)。默认(未配 --fallback-models)时 worker 只记录
fallback 事件(record_fallback() 记 model_fallback 事件,
@@ -122,7 +125,7 @@ import time
from types import SimpleNamespace
from cloud.auth import AuthStore
from cloud.cost import CostStore, estimate_tokens
from cloud.cost import CostStore, estimate_tokens, price_usd_micros, sum_usage
from cloud.db import connect as _db_connect
from cloud.logging_config import get_logger, log_event, setup_logging
from cloud.models import ModelRouter, is_fallbackable, record_fallback
@@ -1455,8 +1458,12 @@ class Worker:
# fallback_models 时保持老行为(router 给出下一模型时只记事件
# 不重试);其它失败(含 worker 超时)按 max_retries 做同模型
# 重试,退避等待后重跑一次 _run_attempt。
# 各次尝试的真实用量都要算进成本(失败尝试花的钱也是钱):
# 主尝试与 fallback 尝试的结果都先累加,最后只记一条汇总。
usage_total = None
for attempt in range(self.max_retries + 1):
result, error = self._run_attempt(task)
usage_total = sum_usage(usage_total, (result or {}).get("usage"))
if self._was_cancelled(task_id) or \
result.get("status") == "cancelled":
break
@@ -1493,6 +1500,8 @@ class Worker:
task = dict(task)
task["model"] = fb
result, error = self._run_attempt(task)
usage_total = sum_usage(usage_total,
(result or {}).get("usage"))
break
if self.router is not None and host_status_try is not None \
and is_fallbackable(host_status_try) and \
@@ -1585,16 +1594,32 @@ class Worker:
finished_at=finished_at)
with self._db_lock:
self.usage_turns += 1
# 成本:host 协议不暴露真实 token 数,用 cost.py 的估算结构
# 记 estimated,绝不标 actual;fallback_count 复用同一记录;
# 模型记最后真正跑的那个(fallback 成功时就是 fallback 模型)。
# cost 记录共用一条连接,并发下必须持锁写,否则两个线程
# 共用同一 sqlite 连接会直接报错。
total = estimate_tokens(task.get("prompt") or "", stdout)
cost_id = self.costs.record(
task_id, task["project_id"], actual_model,
input_tokens=total, output_tokens=0, usd_micros=None,
source="estimated", fallback_count=fallback_count)
# 成本:执行器带回上游真实用量时记 actual(input/output/
# cached 全是实测值,usd 按价目表算、无价模型记 None,
# 报表里进 unpriced_actual_tokens 而不是 0 美元);
# 没带回用量时回落字符估算记 estimated,绝不冒充 actual。
# fallback_count 复用同一记录;模型记最后真正跑的那个
# (fallback 成功时就是 fallback 模型)。cost 记录共用
# 一条连接,并发下必须持锁写,否则两个线程共用同一
# sqlite 连接会直接报错。
if usage_total and (usage_total.get("total_tokens") or 0) > 0:
cost_id = self.costs.record(
task_id, task["project_id"], actual_model,
input_tokens=usage_total.get("input_tokens", 0),
output_tokens=usage_total.get("output_tokens", 0),
cached_input_tokens=usage_total.get(
"cached_input_tokens", 0),
usd_micros=price_usd_micros(
actual_model, usage_total.get("input_tokens", 0),
usage_total.get("cached_input_tokens", 0),
usage_total.get("output_tokens", 0)),
source="actual", fallback_count=fallback_count)
else:
total = estimate_tokens(task.get("prompt") or "", stdout)
cost_id = self.costs.record(
task_id, task["project_id"], actual_model,
input_tokens=total, output_tokens=0, usd_micros=None,
source="estimated", fallback_count=fallback_count)
if final_written:
self._audit(task["project_id"],
"worker_completed" if final == "completed"