niceeval 0.10.3-canary.6 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/niceeval.js +9 -4
- package/dist/agents/types.d.ts +5 -4
- package/dist/context/turn-errors.d.ts +27 -23
- package/dist/i18n/en.d.ts +15 -0
- package/dist/i18n/en.js +16 -1
- package/dist/i18n/zh-CN.d.ts +16 -1
- package/dist/i18n/zh-CN.js +16 -1
- package/dist/o11y/derive.js +28 -24
- package/dist/o11y/types.d.ts +8 -5
- package/dist/report/components/attempt-detail/UsageTable.js +4 -7
- package/dist/report/components/attempt-detail/compute.d.ts +2 -2
- package/dist/report/components/attempt-detail/compute.js +2 -6
- package/dist/report/components/attempt-detail/faces.js +7 -7
- package/dist/report/components/attempt-detail/index.js +0 -3
- package/dist/report/components/entity-lists/EvalList.js +0 -0
- package/dist/report/components/metric-views/compute.js +1 -1
- package/dist/report/model/types.d.ts +3 -4
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +6 -0
- package/dist/results/select.js +8 -0
- package/dist/runner/feedback/sink.d.ts +26 -1
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +105 -7
- package/dist/sandbox/errors.d.ts +29 -0
- package/dist/sandbox/resolve.d.ts +9 -0
- package/dist/shared/failure-class.d.ts +91 -0
- package/dist/types.d.ts +1 -0
- package/dist/util.d.ts +3 -2
- package/dist/util.js +31 -5
- package/docs-site/zh/explanation/runner.mdx +35 -0
- package/docs-site/zh/reference/cli.mdx +10 -1
- package/docs-site/zh/reference/events.mdx +4 -4
- package/docs-site/zh/troubleshooting/debugging.mdx +11 -0
- package/docs-site/zh/tutorials/viewing-results.mdx +35 -3
- package/package.json +28 -24
- package/src/agents/ai-sdk.test.ts +26 -0
- package/src/agents/ai-sdk.ts +7 -4
- package/src/agents/index.ts +5 -3
- package/src/agents/langgraph.test.ts +30 -0
- package/src/agents/langgraph.ts +5 -2
- package/src/agents/openai-compat.test.ts +35 -0
- package/src/agents/openai-compat.ts +16 -4
- package/src/agents/sdk-streams.test.ts +66 -0
- package/src/agents/sdk-streams.ts +3 -1
- package/src/agents/types.ts +5 -4
- package/src/cli.ts +81 -16
- package/src/context/context.test.ts +34 -0
- package/src/context/context.ts +14 -5
- package/src/context/send-retry.test.ts +86 -0
- package/src/context/send-retry.ts +37 -12
- package/src/context/session.ts +24 -0
- package/src/context/turn-errors.test.ts +124 -17
- package/src/context/turn-errors.ts +60 -50
- package/src/define.ts +5 -0
- package/src/i18n/en.ts +22 -1
- package/src/i18n/zh-CN.ts +22 -1
- package/src/index.ts +8 -0
- package/src/o11y/cost.ts +4 -2
- package/src/o11y/derive.test.ts +40 -0
- package/src/o11y/derive.ts +28 -22
- package/src/o11y/otlp/sandbox-receiver.test.ts +201 -0
- package/src/o11y/otlp/sandbox-receiver.ts +73 -27
- package/src/o11y/parsers/bub.test.ts +30 -0
- package/src/o11y/parsers/bub.ts +5 -2
- package/src/o11y/parsers/codex.test.ts +19 -0
- package/src/o11y/parsers/codex.ts +5 -2
- package/src/o11y/types.ts +8 -5
- package/src/report/components/attempt-detail/UsageTable.tsx +4 -6
- package/src/report/components/attempt-detail/attempt-components.test.tsx +5 -8
- package/src/report/components/attempt-detail/compute.ts +2 -7
- package/src/report/components/attempt-detail/faces.ts +7 -7
- package/src/report/components/attempt-detail/index.tsx +0 -3
- package/src/report/components/entity-lists/EvalList.tsx +0 -0
- package/src/report/components/metric-views/compute.ts +1 -1
- package/src/report/model/types.ts +3 -4
- package/src/results/format.ts +9 -2
- package/src/results/index.ts +2 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +132 -21
- package/src/results/select.ts +12 -0
- package/src/results/skipped-notice.ts +0 -0
- package/src/runner/attempt.test.ts +116 -0
- package/src/runner/attempt.ts +89 -8
- package/src/runner/discover.ts +21 -3
- package/src/runner/feedback/coordinator.ts +24 -2
- package/src/runner/feedback/eval-conclusions.ts +6 -3
- package/src/runner/feedback/human.test.ts +300 -6
- package/src/runner/feedback/human.ts +132 -30
- package/src/runner/feedback/json.test.ts +127 -2
- package/src/runner/feedback/json.ts +51 -3
- package/src/runner/feedback/reducer.test.ts +328 -29
- package/src/runner/feedback/reducer.ts +84 -9
- package/src/runner/feedback/sink.ts +39 -1
- package/src/runner/fingerprint.ts +49 -19
- package/src/runner/gate-lease.test.ts +510 -0
- package/src/runner/gate-lease.ts +350 -0
- package/src/runner/lock.test.ts +454 -0
- package/src/runner/lock.ts +288 -0
- package/src/runner/report.test.ts +1 -0
- package/src/runner/run.test.ts +2044 -9
- package/src/runner/run.ts +825 -61
- package/src/runner/teardown-registry.ts +20 -78
- package/src/runner/types.ts +103 -7
- package/src/sandbox/errors.test.ts +72 -0
- package/src/sandbox/errors.ts +83 -0
- package/src/sandbox/keep-registry.ts +22 -46
- package/src/sandbox/resolve.test.ts +100 -0
- package/src/sandbox/resolve.ts +84 -27
- package/src/shared/entry-file-store.test.ts +149 -0
- package/src/shared/entry-file-store.ts +117 -0
- package/src/shared/failure-class.test.ts +137 -0
- package/src/shared/failure-class.ts +175 -0
- package/src/show/index.ts +5 -6
- package/src/show/render.test.ts +116 -15
- package/src/show/render.ts +124 -35
- package/src/types.ts +9 -0
- package/src/util.ts +31 -4
- package/src/view/app/App.tsx +5 -1
- package/src/view/client-dist/app.js +1 -1
- package/src/view/data.ts +5 -9
- package/src/view/shared/types.ts +7 -1
- package/src/view/view-report.test.ts +54 -0
package/src/o11y/derive.ts
CHANGED
|
@@ -61,10 +61,16 @@ function pickExitCode(output: JsonValue | undefined): number | undefined {
|
|
|
61
61
|
// ───────────────────────── deriveRunFacts ─────────────────────────
|
|
62
62
|
|
|
63
63
|
export function deriveRunFacts(events: readonly StreamEvent[]): DerivedFacts {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// 折叠是逐条按发生顺序进行的:called 追加一条新调用,result 回填「当前还没配上 result 的
|
|
65
|
+
// 同 callId 调用」。callId 只在一个 called→result 配对内保证稳定,不保证跨轮唯一——adapter
|
|
66
|
+
// 常按轮各自编号(OpenAI 兼容协议、transcript 归一都会复用 c1/c2…)。所以一个 callId 在其
|
|
67
|
+
// result 之后再次以 called 出现,是新的一次调用,起一条新记录,不覆盖前一轮那条(否则跨轮
|
|
68
|
+
// 聚合会把前面几轮的工具调用抹成「只剩最后一轮」)。用 open*ByCallId 只跟踪各 callId 当前
|
|
69
|
+
// 敞口的那条,配上 result 即关闭。
|
|
70
|
+
const toolCalls: ToolCall[] = [];
|
|
71
|
+
const openToolByCallId = new Map<string, number>();
|
|
72
|
+
const subagentCalls: SubagentCall[] = [];
|
|
73
|
+
const openSubagentByCallId = new Map<string, number>();
|
|
68
74
|
const inputRequests: InputRequest[] = [];
|
|
69
75
|
let messageCount = 0;
|
|
70
76
|
let compactions = 0;
|
|
@@ -81,8 +87,8 @@ export function deriveRunFacts(events: readonly StreamEvent[]): DerivedFacts {
|
|
|
81
87
|
break;
|
|
82
88
|
|
|
83
89
|
case "action.called": {
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
openToolByCallId.set(ev.callId, toolCalls.length);
|
|
91
|
+
toolCalls.push({
|
|
86
92
|
callId: ev.callId,
|
|
87
93
|
name: ev.tool ?? "unknown",
|
|
88
94
|
originalName: ev.name,
|
|
@@ -94,14 +100,14 @@ export function deriveRunFacts(events: readonly StreamEvent[]): DerivedFacts {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
case "action.result": {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
const idx = openToolByCallId.get(ev.callId);
|
|
104
|
+
if (idx !== undefined) {
|
|
105
|
+
toolCalls[idx].output = ev.output;
|
|
106
|
+
toolCalls[idx].status = ev.status;
|
|
107
|
+
openToolByCallId.delete(ev.callId);
|
|
101
108
|
} else {
|
|
102
109
|
// 只有结果、没配上调用:补一条占位 ToolCall。
|
|
103
|
-
|
|
104
|
-
toolCallMap.set(ev.callId, {
|
|
110
|
+
toolCalls.push({
|
|
105
111
|
callId: ev.callId,
|
|
106
112
|
name: "unknown",
|
|
107
113
|
input: null,
|
|
@@ -113,8 +119,8 @@ export function deriveRunFacts(events: readonly StreamEvent[]): DerivedFacts {
|
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
case "subagent.called": {
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
openSubagentByCallId.set(ev.callId, subagentCalls.length);
|
|
123
|
+
subagentCalls.push({
|
|
118
124
|
callId: ev.callId,
|
|
119
125
|
name: ev.name,
|
|
120
126
|
remoteUrl: ev.remoteUrl,
|
|
@@ -125,13 +131,13 @@ export function deriveRunFacts(events: readonly StreamEvent[]): DerivedFacts {
|
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
case "subagent.completed": {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
const idx = openSubagentByCallId.get(ev.callId);
|
|
135
|
+
if (idx !== undefined) {
|
|
136
|
+
subagentCalls[idx].output = ev.output;
|
|
137
|
+
subagentCalls[idx].status = ev.status;
|
|
138
|
+
openSubagentByCallId.delete(ev.callId);
|
|
132
139
|
} else {
|
|
133
|
-
|
|
134
|
-
subagentMap.set(ev.callId, {
|
|
140
|
+
subagentCalls.push({
|
|
135
141
|
callId: ev.callId,
|
|
136
142
|
name: "unknown",
|
|
137
143
|
output: ev.output,
|
|
@@ -164,8 +170,8 @@ export function deriveRunFacts(events: readonly StreamEvent[]): DerivedFacts {
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
return {
|
|
167
|
-
toolCalls
|
|
168
|
-
subagentCalls
|
|
173
|
+
toolCalls,
|
|
174
|
+
subagentCalls,
|
|
169
175
|
inputRequests,
|
|
170
176
|
parked,
|
|
171
177
|
messageCount,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// cases: docs/engineering/testing/unit/experiments-runner.md
|
|
2
|
+
// bug: memory/insandbox-otlp-port-wait-3s-no-retry.md
|
|
3
|
+
// 沙箱内 OTLP 采集器的启动韧性:这条路径外面没有任何重试兜底(命令执行不进 IO 重试、
|
|
4
|
+
// provision 重试只覆盖 create、runner 没有 attempt 级重试),起不来就是一条 errored attempt。
|
|
5
|
+
// 两层 fixture:
|
|
6
|
+
// · 脚本化 fake sandbox —— 重试轮次、每轮换路径、重试前杀上一轮,确定性断言;
|
|
7
|
+
// · 真实 /bin/sh 执行 —— 生成的 shell 脚本本身能跑(语法、算术、退出边),以及采集器进程
|
|
8
|
+
// 一起来就死时不空等满预算(观察实测耗时,不看脚本字节)。
|
|
9
|
+
|
|
10
|
+
import { mkdtemp, rm, writeFile, chmod, readdir } from "node:fs/promises";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { Effect, Exit, Cause } from "effect";
|
|
15
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
16
|
+
import type { CommandResult, Sandbox } from "../../types.ts";
|
|
17
|
+
import { createInSandboxTraceReceiver } from "./sandbox-receiver.ts";
|
|
18
|
+
|
|
19
|
+
const notUsed = () => {
|
|
20
|
+
throw new Error("not used by the in-sandbox receiver");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function baseSandbox(overrides: Partial<Sandbox>): Sandbox {
|
|
24
|
+
return {
|
|
25
|
+
workdir: "/work",
|
|
26
|
+
sandboxId: "fake",
|
|
27
|
+
otlpHost: null,
|
|
28
|
+
runCommand: notUsed,
|
|
29
|
+
runShell: notUsed,
|
|
30
|
+
readFile: notUsed,
|
|
31
|
+
fileExists: notUsed,
|
|
32
|
+
writeFiles: async () => {},
|
|
33
|
+
uploadFiles: notUsed,
|
|
34
|
+
uploadDirectory: notUsed,
|
|
35
|
+
downloadDirectory: notUsed,
|
|
36
|
+
downloadFile: notUsed,
|
|
37
|
+
uploadFile: notUsed,
|
|
38
|
+
stop: async () => {},
|
|
39
|
+
...overrides,
|
|
40
|
+
} as Sandbox;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isStart = (script: string) => script.includes("niceeval-otlp-collector") && script.includes("&");
|
|
44
|
+
const isKill = (script: string) => script.trimStart().startsWith("kill ");
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 启动脚本按轮次回放一条预置 stdout(`PID\n端口`,端口空表示这一轮没等到),日志读取回放
|
|
48
|
+
* 固定内容,kill 一律成功。按脚本形态派发而不是按调用序号——被测实现每轮发几条命令是它自己
|
|
49
|
+
* 的事,fixture 不该把这个数字焊死。
|
|
50
|
+
*/
|
|
51
|
+
function scriptedSandbox(startOutputs: string[], log = "") {
|
|
52
|
+
const shells: string[] = [];
|
|
53
|
+
const written: string[] = [];
|
|
54
|
+
const sandbox = baseSandbox({
|
|
55
|
+
writeFiles: async (files: Record<string, string>) => {
|
|
56
|
+
written.push(...Object.keys(files));
|
|
57
|
+
},
|
|
58
|
+
runShell: async (script: string): Promise<CommandResult> => {
|
|
59
|
+
shells.push(script);
|
|
60
|
+
const stdout = isStart(script) ? (startOutputs.shift() ?? "") : script.startsWith("cat ") ? log : "";
|
|
61
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return { sandbox, shells, written };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 在 Scope 内取端点,并把「此刻为止发过的命令」快照出来——release 阶段的收尾 kill 不混进来。 */
|
|
68
|
+
async function receiverExit(sandbox: Sandbox, shells: string[] = []) {
|
|
69
|
+
return Effect.runPromiseExit(
|
|
70
|
+
Effect.scoped(
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const receiver = yield* createInSandboxTraceReceiver(sandbox);
|
|
73
|
+
return { endpoint: receiver.endpoint(""), shells: [...shells] };
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("沙箱内 OTLP 采集器启动:脚本化 fixture", () => {
|
|
80
|
+
it("第一轮拿不到端口时重试,换一套路径并先杀掉上一轮的进程", async () => {
|
|
81
|
+
// 第一轮:拿到 PID 但端口行为空(采集器慢/死);第二轮:端口写回来了。
|
|
82
|
+
const { sandbox, shells, written } = scriptedSandbox(["4242\n", "4243\n61000"]);
|
|
83
|
+
|
|
84
|
+
const exit = await receiverExit(sandbox, shells);
|
|
85
|
+
|
|
86
|
+
expect(Exit.isSuccess(exit) && exit.value.endpoint).toBe("http://127.0.0.1:61000/v1/traces");
|
|
87
|
+
const sent = Exit.isSuccess(exit) ? exit.value.shells : [];
|
|
88
|
+
expect(sent.filter(isStart)).toHaveLength(2);
|
|
89
|
+
// 上一轮那个「慢但还活着」的采集器必须被杀掉:留着它会占内存,还会在重试之后才把端口
|
|
90
|
+
// 写进它自己那份文件。
|
|
91
|
+
expect(sent.filter(isKill)).toEqual([expect.stringContaining("kill 4242")]);
|
|
92
|
+
// 每轮换一套带随机后缀的脚本路径:上一轮迟到的采集器写不进新一轮的端口 / spans 文件。
|
|
93
|
+
expect(written).toHaveLength(2);
|
|
94
|
+
expect(written[0]).not.toBe(written[1]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("首轮就拿到端口时只起一次,不杀任何进程", async () => {
|
|
98
|
+
const { sandbox, shells } = scriptedSandbox(["4242\n61001"]);
|
|
99
|
+
|
|
100
|
+
const exit = await receiverExit(sandbox, shells);
|
|
101
|
+
|
|
102
|
+
expect(Exit.isSuccess(exit) && exit.value.endpoint).toBe("http://127.0.0.1:61001/v1/traces");
|
|
103
|
+
const sent = Exit.isSuccess(exit) ? exit.value.shells : [];
|
|
104
|
+
expect(sent.filter(isStart)).toHaveLength(1);
|
|
105
|
+
expect(sent.filter(isKill)).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("重试用尽后抛错,带上预算、轮次与采集器自己的日志", async () => {
|
|
109
|
+
const { sandbox, shells } = scriptedSandbox(["4242\n", "4243\n"], "node: not found");
|
|
110
|
+
|
|
111
|
+
const exit = await receiverExit(sandbox, shells);
|
|
112
|
+
|
|
113
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
114
|
+
const message = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined;
|
|
115
|
+
expect(String(message)).toContain("within 20s");
|
|
116
|
+
expect(String(message)).toContain("2 attempts");
|
|
117
|
+
expect(String(message)).toContain("node: not found");
|
|
118
|
+
expect(shells.filter(isStart)).toHaveLength(2);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("沙箱内 OTLP 采集器启动:真实 /bin/sh 执行", () => {
|
|
123
|
+
const dirs: string[] = [];
|
|
124
|
+
const pids: number[] = [];
|
|
125
|
+
|
|
126
|
+
afterEach(async () => {
|
|
127
|
+
for (const pid of pids.splice(0)) {
|
|
128
|
+
try {
|
|
129
|
+
process.kill(pid);
|
|
130
|
+
} catch {
|
|
131
|
+
// 已经退出
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const dir of dirs.splice(0)) await rm(dir, { recursive: true, force: true });
|
|
135
|
+
// 本用例真的会往 /tmp 写采集器脚本 / 端口文件,跑完清掉自己那批。
|
|
136
|
+
for (const name of await readdir(tmpdir())) {
|
|
137
|
+
if (name.startsWith(".niceeval-otlp-")) await rm(join(tmpdir(), name), { force: true }).catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** runShell 真的交给 /bin/sh 跑,writeFiles 真的落盘——生成的脚本语法错误在这里现形。 */
|
|
142
|
+
function shellSandbox(pathPrefix?: string) {
|
|
143
|
+
return baseSandbox({
|
|
144
|
+
writeFiles: async (files: Record<string, string>) => {
|
|
145
|
+
for (const [path, content] of Object.entries(files)) await writeFile(path, content);
|
|
146
|
+
},
|
|
147
|
+
runShell: (script: string) =>
|
|
148
|
+
new Promise<CommandResult>((resolve) => {
|
|
149
|
+
const child = spawn("/bin/sh", ["-c", script], {
|
|
150
|
+
env: { ...process.env, ...(pathPrefix ? { PATH: `${pathPrefix}:${process.env.PATH}` } : {}) },
|
|
151
|
+
});
|
|
152
|
+
let stdout = "";
|
|
153
|
+
let stderr = "";
|
|
154
|
+
child.stdout.on("data", (c) => (stdout += c));
|
|
155
|
+
child.stderr.on("data", (c) => (stderr += c));
|
|
156
|
+
child.on("close", (code) => resolve({ stdout, stderr, exitCode: code ?? 0 }));
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
it("真实 shell 下起得来:端口写回宿主,采集器确实在监听", async () => {
|
|
162
|
+
const sandbox = shellSandbox();
|
|
163
|
+
|
|
164
|
+
const result = await Effect.runPromise(
|
|
165
|
+
Effect.scoped(
|
|
166
|
+
Effect.gen(function* () {
|
|
167
|
+
const receiver = yield* createInSandboxTraceReceiver(sandbox);
|
|
168
|
+
const endpoint = receiver.endpoint("");
|
|
169
|
+
const status = yield* Effect.promise(async () => {
|
|
170
|
+
const res = await fetch(endpoint, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "content-type": "application/json" },
|
|
173
|
+
body: "{}",
|
|
174
|
+
});
|
|
175
|
+
return res.status;
|
|
176
|
+
});
|
|
177
|
+
return { endpoint, status };
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result.endpoint).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/v1\/traces$/);
|
|
183
|
+
expect(result.status).toBe(200);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("采集器一起来就死时不空等满预算,错误里带着它的日志", async () => {
|
|
187
|
+
const shimDir = await mkdtemp(join(tmpdir(), "niceeval-node-shim-"));
|
|
188
|
+
dirs.push(shimDir);
|
|
189
|
+
await writeFile(join(shimDir, "node"), '#!/bin/sh\necho "node: simulated crash" >&2\nexit 1\n');
|
|
190
|
+
await chmod(join(shimDir, "node"), 0o755);
|
|
191
|
+
|
|
192
|
+
const startedAt = Date.now();
|
|
193
|
+
const exit = await receiverExit(shellSandbox(shimDir));
|
|
194
|
+
const elapsed = Date.now() - startedAt;
|
|
195
|
+
|
|
196
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
197
|
+
expect(String(Exit.isFailure(exit) ? Cause.squash(exit.cause) : "")).toContain("node: simulated crash");
|
|
198
|
+
// 进程已死时循环立刻 break,两轮加起来也远小于一轮的等待预算(20s)。
|
|
199
|
+
expect(elapsed).toBeLessThan(5_000);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// 6. close() 尝试 kill PID(沙箱本身也会停,所以 best-effort)
|
|
12
12
|
//
|
|
13
13
|
// 文件路径带随机后缀:同一沙箱跨 eval 复用时,每个 receiver 实例的脚本 / spans /
|
|
14
|
-
// 端口文件互不串扰,collector 也不会读到上一个 eval 的 span
|
|
14
|
+
// 端口文件互不串扰,collector 也不会读到上一个 eval 的 span。启动重试同样每轮换一套后缀
|
|
15
|
+
// (见 startCollector)。
|
|
15
16
|
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { Effect } from "effect";
|
|
@@ -56,6 +57,16 @@ server.listen(0, '127.0.0.1', () => {
|
|
|
56
57
|
`;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// 端口等待预算。这条路径外面没有任何一层重试兜着——`runShell` 不进 `withSandboxIoRetry`
|
|
61
|
+
// (命令执行有不可重复副作用),`withProvisionRetry` 只覆盖沙箱 create,runner 也没有
|
|
62
|
+
// attempt 级重试:等不到端口就是一条 errored attempt,agent 一次都没跑
|
|
63
|
+
// (memory/insandbox-otlp-port-wait-3s-no-retry.md)。所以预算按冷沙箱首次起 node 的最坏
|
|
64
|
+
// 情况给,而不是按热沙箱的常见值给。
|
|
65
|
+
const PORT_WAIT_MS = 20_000;
|
|
66
|
+
// 启动整体的重试次数。慢不需要重试(一次等满预算就够),这里兜的是 collector 起来就死
|
|
67
|
+
// (镜像里没有 node、脚本被 OOM kill 等)——那种情况下沙箱侧循环会立刻 break,重试很便宜。
|
|
68
|
+
const START_ATTEMPTS = 2;
|
|
69
|
+
|
|
59
70
|
export function createInSandboxTraceReceiver(sandbox: Sandbox) {
|
|
60
71
|
return Effect.acquireRelease(
|
|
61
72
|
Effect.promise(() => makeInSandboxReceiver(sandbox)),
|
|
@@ -63,35 +74,70 @@ export function createInSandboxTraceReceiver(sandbox: Sandbox) {
|
|
|
63
74
|
);
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
interface StartedCollector {
|
|
78
|
+
pid: number;
|
|
79
|
+
port: number;
|
|
80
|
+
spansPath: string;
|
|
81
|
+
}
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
/**
|
|
84
|
+
* 上传脚本 + 后台起 collector + 等端口文件,失败重试。每一轮换一套带随机后缀的路径:
|
|
85
|
+
* 上一轮那个「慢但还活着」的 collector 如果在重试之后才起来,会写回它自己那份端口 / spans
|
|
86
|
+
* 文件,不会污染新一轮。
|
|
87
|
+
*/
|
|
88
|
+
async function startCollector(sandbox: Sandbox): Promise<StartedCollector> {
|
|
89
|
+
let lastLog = "";
|
|
90
|
+
for (let attempt = 1; ; attempt++) {
|
|
91
|
+
const tag = randomUUID().slice(0, 8);
|
|
92
|
+
const collectorPath = `/tmp/.niceeval-otlp-collector-${tag}.cjs`;
|
|
93
|
+
const spansPath = `/tmp/.niceeval-otlp-spans-${tag}.jsonl`;
|
|
94
|
+
const portPath = `/tmp/.niceeval-otlp-port-${tag}`;
|
|
95
|
+
const logPath = `/tmp/.niceeval-otlp-collector-${tag}.log`;
|
|
96
|
+
|
|
97
|
+
await sandbox.writeFiles({ [collectorPath]: collectorScript(spansPath, portPath) });
|
|
98
|
+
|
|
99
|
+
// 后台启动 + 等端口文件,折进一次 shell 往返(远程沙箱一次 exec 要 100-500ms,
|
|
100
|
+
// host 侧逐次轮询会把几秒的启动等待放大成 N 个 API round-trip)。循环两条退出边:
|
|
101
|
+
// · `kill -0` 失败 → collector 已经死了,别再空等满预算,立刻回 host 重试;
|
|
102
|
+
// · 到 deadline → 真的太慢。
|
|
103
|
+
// 预算按 `date +%s` 的墙钟算而不是数 tick:`sleep` 的小数秒支持因镜像而异,数 tick 会让
|
|
104
|
+
// 不支持小数的镜像瞬间跑完循环、伪装成「等了 20s」。
|
|
105
|
+
// 输出两行:PID、端口(等不到则空)。
|
|
106
|
+
const startResult = await sandbox.runShell(
|
|
107
|
+
`node ${collectorPath} >${logPath} 2>&1 & pid=$!; echo $pid; ` +
|
|
108
|
+
`end=$(( $(date +%s) + ${Math.ceil(PORT_WAIT_MS / 1000)} )); ` +
|
|
109
|
+
`while [ ! -s ${portPath} ]; do ` +
|
|
110
|
+
`kill -0 $pid 2>/dev/null || break; ` +
|
|
111
|
+
`[ "$(date +%s)" -lt "$end" ] || break; ` +
|
|
112
|
+
`sleep 0.1 2>/dev/null || sleep 1; ` +
|
|
113
|
+
`done; ` +
|
|
114
|
+
`cat ${portPath} 2>/dev/null || true`,
|
|
93
115
|
);
|
|
116
|
+
const [pidLine, portLine] = startResult.stdout.trim().split("\n");
|
|
117
|
+
const pid = parseInt((pidLine ?? "").trim(), 10);
|
|
118
|
+
const port = parseInt((portLine ?? "").trim(), 10) || 0;
|
|
119
|
+
if (port) return { pid, port, spansPath };
|
|
120
|
+
|
|
121
|
+
const log = await sandbox.runShell(`cat ${logPath} 2>/dev/null || true`).catch(() => undefined);
|
|
122
|
+
lastLog = log?.stdout.trim() ?? "";
|
|
123
|
+
// 这一轮可能只是慢、进程还活着:重试前先杀掉,不留孤儿 collector 占内存
|
|
124
|
+
//(沙箱复用 / --keep-sandbox 下它会一直在)。
|
|
125
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
126
|
+
await sandbox.runShell(`kill ${pid} 2>/dev/null || true`).catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
if (attempt >= START_ATTEMPTS) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`in-sandbox OTLP collector failed to report its port within ${Math.round(PORT_WAIT_MS / 1000)}s ` +
|
|
131
|
+
`(${attempt} attempts). Collector log:\n${lastLog || "(empty)"}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
94
134
|
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function makeInSandboxReceiver(sandbox: Sandbox): Promise<TraceReceiver> {
|
|
138
|
+
let cached: TraceSpan[] = [];
|
|
139
|
+
|
|
140
|
+
const { pid, port, spansPath } = await startCollector(sandbox);
|
|
95
141
|
|
|
96
142
|
return {
|
|
97
143
|
endpoint: (_host) => `http://127.0.0.1:${port}/v1/traces`,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// cases: docs/engineering/testing/unit/results.md
|
|
2
|
+
// 「Usage、facts 与失败命令证据落盘」桶恒互斥归一:bub tape 的 usage 是 Chat Completions
|
|
3
|
+
// 形状,prompt_tokens_details.cached_tokens 是 prompt_tokens 子集,聚合前扣减;
|
|
4
|
+
// 同对象的 cost 是实测计费,照常累进 costUSD。
|
|
5
|
+
// bug: memory/estimatecost-openai-inclusive-cache-double-billed.md
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { parseBubTranscript } from "./bub.ts";
|
|
10
|
+
|
|
11
|
+
describe("parseBubTranscript usage 归一(OpenAI 口径)", () => {
|
|
12
|
+
it("cached_tokens 从 prompt_tokens 里扣出,cost 累进实测 costUSD", () => {
|
|
13
|
+
const line = JSON.stringify({
|
|
14
|
+
kind: "event",
|
|
15
|
+
payload: {
|
|
16
|
+
name: "run",
|
|
17
|
+
data: {
|
|
18
|
+
usage: {
|
|
19
|
+
prompt_tokens: 1000,
|
|
20
|
+
completion_tokens: 20,
|
|
21
|
+
prompt_tokens_details: { cached_tokens: 900 },
|
|
22
|
+
cost: 0.05,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const parsed = parseBubTranscript(line);
|
|
28
|
+
expect(parsed.usage).toMatchObject({ inputTokens: 100, cacheReadTokens: 900, outputTokens: 20, costUSD: 0.05 });
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/o11y/parsers/bub.ts
CHANGED
|
@@ -93,12 +93,15 @@ export function parseBubTranscript(raw: string | undefined): ParsedTranscript {
|
|
|
93
93
|
// 从 event/run 的 data.usage(或 legacy 顶层 usage)累加用量。
|
|
94
94
|
const addUsage = (usage: unknown): void => {
|
|
95
95
|
if (!usage || typeof usage !== "object") return;
|
|
96
|
-
const
|
|
96
|
+
const rawInput = num(usage, "input_tokens", "prompt_tokens", "inputTokens");
|
|
97
97
|
const output = num(usage, "output_tokens", "completion_tokens", "outputTokens");
|
|
98
98
|
let cache = num(usage, "cached_input_tokens", "cache_read_input_tokens", "cacheReadTokens");
|
|
99
99
|
if (cache === 0) cache = num(get(usage, "prompt_tokens_details"), "cached_tokens");
|
|
100
100
|
const cost = num(usage, "cost");
|
|
101
|
-
if (
|
|
101
|
+
if (rawInput === 0 && output === 0 && cache === 0 && cost === 0) return;
|
|
102
|
+
// Chat Completions 口径的 prompt_tokens 含缓存命中,cached_tokens 是其子集;
|
|
103
|
+
// 落互斥桶前扣掉(docs/feature/adapters/sdk/bub/cost.md)
|
|
104
|
+
const input = Math.max(0, rawInput - cache);
|
|
102
105
|
inputTokens += input;
|
|
103
106
|
outputTokens += output;
|
|
104
107
|
cacheReadTokens += cache;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// cases: docs/engineering/testing/unit/results.md
|
|
2
|
+
// 「Usage、facts 与失败命令证据落盘」桶恒互斥归一:codex transcript 的 cached_input_tokens
|
|
3
|
+
// 是 input_tokens 子集,聚合前逐请求扣减。
|
|
4
|
+
// bug: memory/estimatecost-openai-inclusive-cache-double-billed.md
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { parseCodexTranscript } from "./codex.ts";
|
|
9
|
+
|
|
10
|
+
describe("parseCodexTranscript usage 归一(OpenAI 口径)", () => {
|
|
11
|
+
it("cached_input_tokens 从 input_tokens 里扣出,cache 单独成桶", () => {
|
|
12
|
+
const raw = [
|
|
13
|
+
JSON.stringify({ type: "turn.completed", usage: { input_tokens: 1000, cached_input_tokens: 900, output_tokens: 50 } }),
|
|
14
|
+
JSON.stringify({ type: "turn.completed", usage: { input_tokens: 2000, cached_input_tokens: 1700, output_tokens: 30 } }),
|
|
15
|
+
].join("\n");
|
|
16
|
+
const parsed = parseCodexTranscript(raw);
|
|
17
|
+
expect(parsed.usage).toMatchObject({ inputTokens: 400, cacheReadTokens: 2600, outputTokens: 80, requests: 2 });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -80,14 +80,17 @@ function readUsage(u: unknown): { input: number; output: number; cacheRead: numb
|
|
|
80
80
|
}
|
|
81
81
|
return 0;
|
|
82
82
|
};
|
|
83
|
-
const
|
|
84
|
-
const output = num("output_tokens", "completion_tokens", "outputTokens", "completionTokens");
|
|
83
|
+
const rawInput = num("input_tokens", "prompt_tokens", "inputTokens", "promptTokens");
|
|
85
84
|
const cacheRead = num(
|
|
86
85
|
"cached_input_tokens",
|
|
87
86
|
"cache_read_input_tokens",
|
|
88
87
|
"cache_read_tokens",
|
|
89
88
|
"cacheReadTokens",
|
|
90
89
|
);
|
|
90
|
+
// codex-rs TokenUsage 的 cached_input_tokens 是 input_tokens 的子集,落互斥桶前扣掉
|
|
91
|
+
// (docs/feature/adapters/sdk/codex-cli/cost.md)
|
|
92
|
+
const input = Math.max(0, rawInput - cacheRead);
|
|
93
|
+
const output = num("output_tokens", "completion_tokens", "outputTokens", "completionTokens");
|
|
91
94
|
// codex-rs TokenUsage 结构体与 Responses API usage.output_tokens_details 都拆出这项:
|
|
92
95
|
// 推理模型(o-series/gpt-5 系列)的思考 token,已计入 output 但值得单列展示。
|
|
93
96
|
const reasoning = num("reasoning_output_tokens", "reasoning_tokens", "reasoningOutputTokens", "reasoningTokens");
|
package/src/o11y/types.ts
CHANGED
|
@@ -6,18 +6,21 @@ import type { JsonValue, SourceLoc } from "../shared/types.ts";
|
|
|
6
6
|
/**
|
|
7
7
|
* 一次运行的 token 用量(沙箱型从 transcript/OTel span 的 `gen_ai.usage.*` 属性抠,remote 由
|
|
8
8
|
* send 的 `Turn.usage` 直接返回)。每个字段只在协议真实提供该值时存在——原始协议没有 usage 时
|
|
9
|
-
* 省略,不编造数值;不存在「默认 0」或「默认 1
|
|
9
|
+
* 省略,不编造数值;不存在「默认 0」或「默认 1」的字段。三个输入侧桶恒互斥:相加才是送进模型的
|
|
10
|
+
* 完整上下文量;把协议原生口径归一到互斥是 adapter 的落值义务——OpenAI 系报「含缓存的输入总量 +
|
|
11
|
+
* 缓存命中子集」,落 inputTokens 前先扣掉子集(docs/feature/results/architecture.md#usage,
|
|
12
|
+
* 各协议明细见 docs/feature/adapters/sdk/<name>/cost.md)。
|
|
10
13
|
*/
|
|
11
14
|
export interface Usage {
|
|
12
|
-
/**
|
|
15
|
+
/** 未命中缓存、按全价计费的输入 token;与两个 cache 桶互斥。 */
|
|
13
16
|
inputTokens?: number;
|
|
14
17
|
/** 输出(completion)token 数。 */
|
|
15
18
|
outputTokens?: number;
|
|
16
|
-
/**
|
|
19
|
+
/** 从提示缓存命中的输入 token;独立计价桶,不包含在 inputTokens 里(省略表示该 agent 不上报此项)。 */
|
|
17
20
|
cacheReadTokens?: number;
|
|
18
|
-
/**
|
|
21
|
+
/** 写入提示缓存的输入 token;独立计价桶,不包含在 inputTokens 里(省略表示该 agent 不上报此项)。 */
|
|
19
22
|
cacheCreationTokens?: number;
|
|
20
|
-
/** 推理(thinking)token
|
|
23
|
+
/** 推理(thinking)token 数,outputTokens 的已含明细,单列展示用;只在协议真实提供时存在。 */
|
|
21
24
|
reasoningTokens?: number;
|
|
22
25
|
/** 真实发生的模型请求数。协议不提供请求计数就省略,绝不写 1 凑数。 */
|
|
23
26
|
requests?: number;
|
|
@@ -12,12 +12,10 @@ export function UsageTable({ data, className }: { data: UsageTableData | null; c
|
|
|
12
12
|
const rows: [string, string][] = [];
|
|
13
13
|
if (data.turns !== undefined) rows.push(["turns", String(data.turns)]);
|
|
14
14
|
if (data.toolCalls !== undefined) rows.push(["tool calls", String(data.toolCalls)]);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
if (
|
|
18
|
-
rows.push(["uncached in",
|
|
19
|
-
} else if (usage?.inputTokens !== undefined) {
|
|
20
|
-
rows.push(["in", usage.inputTokens.toLocaleString()]);
|
|
15
|
+
// 桶恒互斥,inputTokens 就是未缓存输入;"uncached in" 标签只在 cache 拆分在场时用,
|
|
16
|
+
// cache 桶缺席的数字不贴标注。
|
|
17
|
+
if (usage?.inputTokens !== undefined) {
|
|
18
|
+
rows.push([usage.cacheReadTokens !== undefined ? "uncached in" : "in", usage.inputTokens.toLocaleString()]);
|
|
21
19
|
}
|
|
22
20
|
if (usage?.cacheReadTokens !== undefined) rows.push(["cache read", usage.cacheReadTokens.toLocaleString()]);
|
|
23
21
|
if (usage?.outputTokens !== undefined) rows.push(["out", usage.outputTokens.toLocaleString()]);
|
|
@@ -422,15 +422,12 @@ describe("Attempt 详情组件族:非空/空证据矩阵", () => {
|
|
|
422
422
|
expect("turns" in data).toBe(true);
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
-
it("UsageTable
|
|
425
|
+
it("UsageTable:桶恒互斥,inputTokens 落盘原样透传,不派生第二个输入字段", () => {
|
|
426
|
+
// inputTokens 本身就是未缓存输入(docs/feature/results/architecture.md#usage),
|
|
427
|
+
// "uncached in" 只是 face 层在 cacheReadTokens 在场时给的标注,data 不多一个字段。
|
|
426
428
|
const both = usageTableData(evidenceOf({ result: resultOf({ usage: { inputTokens: 100, outputTokens: 1, cacheReadTokens: 40 } }) }))!;
|
|
427
|
-
expect(both.
|
|
428
|
-
|
|
429
|
-
// 只缺 cacheReadTokens(该 agent 不上报缓存命中):不派生,整字段省略(不回退猜 0)——
|
|
430
|
-
// text 面此时回退显示原始 inputTokens,见 faces.ts::usageTableText。
|
|
431
|
-
const noCacheRead = usageTableData(evidenceOf({ result: resultOf({ usage: { inputTokens: 100, outputTokens: 1 } }) }))!;
|
|
432
|
-
expect(noCacheRead.uncachedInputTokens).toBeUndefined();
|
|
433
|
-
expect("uncachedInputTokens" in noCacheRead).toBe(false);
|
|
429
|
+
expect(both.usage?.inputTokens).toBe(100);
|
|
430
|
+
expect("uncachedInputTokens" in both).toBe(false);
|
|
434
431
|
});
|
|
435
432
|
|
|
436
433
|
it("UsageTable:requests 缺失时不出现在 usage 对象里(落盘原样透传,不由 usageTableData 凑值)", () => {
|
|
@@ -486,8 +486,8 @@ export function attemptDiagnosticsData(evidence: AttemptEvidence): AttemptDiagno
|
|
|
486
486
|
* identity 字段(locator/experimentId/evalId/attempt/verdict)恒有;turns/toolCalls 是 events
|
|
487
487
|
* 派生(与 o11y.json 行为摘要同源,buildO11ySummary 与 o11y.json 落盘走同一份纯函数),没有
|
|
488
488
|
* events 就整对省略——不因为其中一个恰好是 0 就当作"缺失"处理,0 是观测到的事实。
|
|
489
|
-
*
|
|
490
|
-
*
|
|
489
|
+
* token 桶恒互斥,inputTokens 本身就是未缓存输入,不派生第二个字段。
|
|
490
|
+
* turns/toolCalls/usage 三者全部缺失时返回 null——
|
|
491
491
|
* 没有任何用量事实可摆,与其余叶子同一条"没有 usage 时零输出"规则。
|
|
492
492
|
*/
|
|
493
493
|
export function usageTableData(evidence: AttemptEvidence): UsageTableData | null {
|
|
@@ -498,10 +498,6 @@ export function usageTableData(evidence: AttemptEvidence): UsageTableData | null
|
|
|
498
498
|
const usage = result.usage;
|
|
499
499
|
if (turns === undefined && toolCalls === undefined && usage === undefined) return null;
|
|
500
500
|
|
|
501
|
-
const uncachedInputTokens =
|
|
502
|
-
usage && typeof usage.inputTokens === "number" && typeof usage.cacheReadTokens === "number"
|
|
503
|
-
? usage.inputTokens - usage.cacheReadTokens
|
|
504
|
-
: undefined;
|
|
505
501
|
const estimatedCostUSD = attemptCostUSD(result);
|
|
506
502
|
|
|
507
503
|
return {
|
|
@@ -513,7 +509,6 @@ export function usageTableData(evidence: AttemptEvidence): UsageTableData | null
|
|
|
513
509
|
...(turns !== undefined ? { turns } : {}),
|
|
514
510
|
...(toolCalls !== undefined ? { toolCalls } : {}),
|
|
515
511
|
...(usage !== undefined ? { usage } : {}),
|
|
516
|
-
...(uncachedInputTokens !== undefined ? { uncachedInputTokens } : {}),
|
|
517
512
|
...(estimatedCostUSD !== null ? { estimatedCostUSD } : {}),
|
|
518
513
|
};
|
|
519
514
|
}
|
|
@@ -314,14 +314,14 @@ export function usageTableText(data: UsageTableData | null, _ctx: TextContext):
|
|
|
314
314
|
if (data === null) return "";
|
|
315
315
|
const usage = data.usage;
|
|
316
316
|
|
|
317
|
-
//
|
|
318
|
-
//
|
|
317
|
+
// 桶恒互斥,inputTokens 就是未缓存输入;"uncached" 标注只在 cache 拆分真实在场时给,
|
|
318
|
+
// cache 桶缺席的数字不贴标注——不给没有拆分事实的数字暗示拆分。
|
|
319
319
|
const inFragment =
|
|
320
|
-
|
|
321
|
-
?
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
320
|
+
usage?.inputTokens !== undefined
|
|
321
|
+
? usage.cacheReadTokens !== undefined
|
|
322
|
+
? `${formatMetricValue(usage.inputTokens)} uncached in`
|
|
323
|
+
: `${formatMetricValue(usage.inputTokens)} in`
|
|
324
|
+
: undefined;
|
|
325
325
|
const cacheFragment = usage?.cacheReadTokens !== undefined ? `${formatMetricValue(usage.cacheReadTokens)} cache read` : undefined;
|
|
326
326
|
const outFragment = usage?.outputTokens !== undefined ? `${formatMetricValue(usage.outputTokens)} out` : undefined;
|
|
327
327
|
const inCache = [inFragment, cacheFragment].filter((s): s is string => s !== undefined).join(" + ");
|
|
@@ -594,9 +594,6 @@ export function validateUsageData(data: unknown): string | null {
|
|
|
594
594
|
const usageProb = usageProblem(data.usage, "usage");
|
|
595
595
|
if (usageProb !== null) return usageProb;
|
|
596
596
|
}
|
|
597
|
-
if (data.uncachedInputTokens !== undefined && typeof data.uncachedInputTokens !== "number") {
|
|
598
|
-
return '"uncachedInputTokens" must be a number';
|
|
599
|
-
}
|
|
600
597
|
if (data.estimatedCostUSD !== undefined && typeof data.estimatedCostUSD !== "number") {
|
|
601
598
|
return '"estimatedCostUSD" must be a number';
|
|
602
599
|
}
|