逐任务真实成本:上游用量事件 + 价目表计价
此前每任务只记一条 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:
1 parent
353d6c1aa8
commit
63823fc2e3
11 files changed
+709
-67
No files matched your search
@@ -572,6 +572,85 @@ class GithubPRTest(unittest.TestCase):
|
||||
self.assertIn("remote", r.stdout + r.stderr)
|
||||
|
||||
|
||||
class TurnUsageSumTest(unittest.TestCase):
|
||||
def _note(self, thread_id, turn_id, last):
|
||||
return {"method": "thread/tokenUsage/updated",
|
||||
"params": {"threadId": thread_id, "turnId": turn_id,
|
||||
"tokenUsage": {"last": last}}}
|
||||
|
||||
def test_sums_last_across_completions(self):
|
||||
# 2026-10-26 中继实测前两次通知:last 之和等于第二次的 total。
|
||||
evs = [
|
||||
self._note("t1", "u1", {
|
||||
"inputTokens": 10906, "cachedInputTokens": 9192,
|
||||
"cacheWriteInputTokens": 0, "outputTokens": 346,
|
||||
"reasoningOutputTokens": 256, "totalTokens": 11252}),
|
||||
self._note("t1", "u1", {
|
||||
"inputTokens": 11041, "cachedInputTokens": 9192,
|
||||
"cacheWriteInputTokens": 0, "outputTokens": 230,
|
||||
"reasoningOutputTokens": 161, "totalTokens": 11271}),
|
||||
]
|
||||
self.assertEqual(host._sum_turn_usage(evs, "t1", "u1"), {
|
||||
"input_tokens": 21947, "cached_input_tokens": 18384,
|
||||
"cache_write_input_tokens": 0, "output_tokens": 576,
|
||||
"reasoning_output_tokens": 417, "total_tokens": 22523,
|
||||
"completions": 2})
|
||||
|
||||
def test_filters_by_thread_and_turn(self):
|
||||
evs = [
|
||||
self._note("t1", "u1", {
|
||||
"inputTokens": 10, "cachedInputTokens": 0,
|
||||
"cacheWriteInputTokens": 0, "outputTokens": 5,
|
||||
"reasoningOutputTokens": 0, "totalTokens": 15}),
|
||||
self._note("t1", "u2", {
|
||||
"inputTokens": 999, "cachedInputTokens": 0,
|
||||
"cacheWriteInputTokens": 0, "outputTokens": 999,
|
||||
"reasoningOutputTokens": 0, "totalTokens": 1998}),
|
||||
self._note("t2", "u1", {
|
||||
"inputTokens": 999, "cachedInputTokens": 0,
|
||||
"cacheWriteInputTokens": 0, "outputTokens": 999,
|
||||
"reasoningOutputTokens": 0, "totalTokens": 1998}),
|
||||
{"method": "item/completed", "params": {}},
|
||||
]
|
||||
usage = host._sum_turn_usage(evs, "t1", "u1")
|
||||
self.assertEqual(usage["input_tokens"], 10)
|
||||
self.assertEqual(usage["completions"], 1)
|
||||
self.assertIsNone(host._sum_turn_usage(evs, "t1", "u9"))
|
||||
self.assertIsNone(host._sum_turn_usage([], "t1", "u1"))
|
||||
|
||||
def test_retry_sums_usage_across_attempts(self):
|
||||
class FakeServer:
|
||||
def __init__(self, results):
|
||||
self.results = list(results)
|
||||
|
||||
def run_turn(self, thread_id, prompt, model=None, effort=None,
|
||||
sandbox=None):
|
||||
return self.results.pop(0)
|
||||
|
||||
task = {"name": "t", "thread_id": "x", "sandbox": None,
|
||||
"max_retries": 1, "retry_count": 0, "turns": []}
|
||||
state = {"tasks": {"t": task}, "automations": []}
|
||||
srv = FakeServer([
|
||||
{"status": "failed", "text": "", "usage": {
|
||||
"input_tokens": 100, "cached_input_tokens": 0,
|
||||
"cache_write_input_tokens": 0, "output_tokens": 10,
|
||||
"reasoning_output_tokens": 0, "total_tokens": 110,
|
||||
"completions": 1}},
|
||||
{"status": "completed", "text": "ok", "usage": {
|
||||
"input_tokens": 200, "cached_input_tokens": 50,
|
||||
"cache_write_input_tokens": 0, "output_tokens": 20,
|
||||
"reasoning_output_tokens": 5, "total_tokens": 220,
|
||||
"completions": 1}},
|
||||
])
|
||||
result = host.run_task_turn_with_retry(
|
||||
srv, state, task, "do it", "m", "low", cfg={})
|
||||
self.assertEqual(result["status"], "completed")
|
||||
self.assertEqual(result["usage"]["input_tokens"], 300)
|
||||
self.assertEqual(result["usage"]["cached_input_tokens"], 50)
|
||||
self.assertEqual(result["usage"]["total_tokens"], 330)
|
||||
self.assertEqual(result["usage"]["completions"], 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in new issue
Block a user