pi-nebius 0.3.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/CONTRIBUTING.md +39 -0
  3. package/LICENSE +21 -0
  4. package/README.md +239 -0
  5. package/SECURITY.md +47 -0
  6. package/benchmarks/add-api-endpoint/benchmark.yaml +13 -0
  7. package/benchmarks/add-api-endpoint/fixture/app.mjs +6 -0
  8. package/benchmarks/add-api-endpoint/fixture/app.test.mjs +8 -0
  9. package/benchmarks/add-api-endpoint/fixture/package.json +8 -0
  10. package/benchmarks/add-api-endpoint/validation/check.test.mjs +37 -0
  11. package/benchmarks/fix-auth-bug/benchmark.yaml +14 -0
  12. package/benchmarks/fix-auth-bug/fixture/auth.mjs +4 -0
  13. package/benchmarks/fix-auth-bug/fixture/auth.test.mjs +26 -0
  14. package/benchmarks/fix-auth-bug/fixture/package.json +8 -0
  15. package/benchmarks/fix-auth-bug/validation/check.test.mjs +25 -0
  16. package/benchmarks/multi-file-feature/benchmark.yaml +15 -0
  17. package/benchmarks/multi-file-feature/fixture/package.json +8 -0
  18. package/benchmarks/multi-file-feature/fixture/routes.mjs +11 -0
  19. package/benchmarks/multi-file-feature/fixture/routes.test.mjs +14 -0
  20. package/benchmarks/multi-file-feature/fixture/serialize.mjs +3 -0
  21. package/benchmarks/multi-file-feature/fixture/store.mjs +10 -0
  22. package/benchmarks/multi-file-feature/validation/check.test.mjs +52 -0
  23. package/benchmarks/refactor-module/benchmark.yaml +12 -0
  24. package/benchmarks/refactor-module/fixture/invoice.mjs +8 -0
  25. package/benchmarks/refactor-module/fixture/invoice.test.mjs +9 -0
  26. package/benchmarks/refactor-module/fixture/package.json +8 -0
  27. package/benchmarks/refactor-module/validation/check.test.mjs +36 -0
  28. package/dist/benchmark/cli.js +112 -0
  29. package/dist/benchmark/command.js +194 -0
  30. package/dist/benchmark/definition.js +109 -0
  31. package/dist/benchmark/host-worker.js +14 -0
  32. package/dist/benchmark/instrumentation.js +296 -0
  33. package/dist/benchmark/metrics.js +78 -0
  34. package/dist/benchmark/process.js +122 -0
  35. package/dist/benchmark/project.js +70 -0
  36. package/dist/benchmark/report.js +94 -0
  37. package/dist/benchmark/runner.js +376 -0
  38. package/dist/benchmark/types.js +1 -0
  39. package/dist/benchmark/worker.js +134 -0
  40. package/dist/benchmark/workspace.js +55 -0
  41. package/dist/discovery.js +154 -0
  42. package/dist/errors.js +32 -0
  43. package/dist/index.js +86 -0
  44. package/dist/model-settings-command.js +130 -0
  45. package/dist/model-settings.js +101 -0
  46. package/dist/models.js +62 -0
  47. package/dist/provider.js +48 -0
  48. package/docs/benchmark-research.md +35 -0
  49. package/docs/benchmarking.md +253 -0
  50. package/docs/security-review.md +49 -0
  51. package/docs/validation.md +51 -0
  52. package/examples/models.json +31 -0
  53. package/package.json +74 -0
  54. package/src/benchmark/cli.ts +118 -0
  55. package/src/benchmark/command.ts +218 -0
  56. package/src/benchmark/definition.ts +112 -0
  57. package/src/benchmark/host-worker.ts +14 -0
  58. package/src/benchmark/instrumentation.ts +298 -0
  59. package/src/benchmark/metrics.ts +101 -0
  60. package/src/benchmark/process.ts +120 -0
  61. package/src/benchmark/project.ts +71 -0
  62. package/src/benchmark/report.ts +111 -0
  63. package/src/benchmark/runner.ts +458 -0
  64. package/src/benchmark/types.ts +180 -0
  65. package/src/benchmark/worker.ts +150 -0
  66. package/src/benchmark/workspace.ts +64 -0
  67. package/src/discovery.ts +176 -0
  68. package/src/errors.ts +32 -0
  69. package/src/index.ts +96 -0
  70. package/src/model-settings-command.ts +151 -0
  71. package/src/model-settings.ts +129 -0
  72. package/src/models.ts +73 -0
  73. package/src/provider.ts +63 -0
@@ -0,0 +1,296 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isRecord } from "../models.js";
3
+ export const hash = (text) => createHash("sha256").update(text).digest("hex");
4
+ const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
5
+ export function reportedUsage(value) {
6
+ if (!isRecord(value))
7
+ return null;
8
+ const prompt = isRecord(value.prompt_tokens_details) ? value.prompt_tokens_details : {};
9
+ const completion = isRecord(value.completion_tokens_details)
10
+ ? value.completion_tokens_details
11
+ : {};
12
+ return {
13
+ inputTokens: count(value.prompt_tokens),
14
+ outputTokens: count(value.completion_tokens),
15
+ cachedInputTokens: count(prompt.cached_tokens ?? value.prompt_cache_hit_tokens ?? value.cached_tokens),
16
+ reasoningTokens: count(completion.reasoning_tokens),
17
+ totalTokens: count(value.total_tokens),
18
+ };
19
+ }
20
+ export function emptyObservation() {
21
+ return {
22
+ requests: [],
23
+ tools: [],
24
+ agentTurns: 0,
25
+ assistantMessages: 0,
26
+ toolCalls: 0,
27
+ toolCallsByType: {},
28
+ compactions: 0,
29
+ retries: 0,
30
+ agentStartedAtMs: null,
31
+ agentEndedAtMs: null,
32
+ systemPromptHash: null,
33
+ lastAssistantStopReason: null,
34
+ errors: [],
35
+ };
36
+ }
37
+ export function redactor(secrets) {
38
+ return (value) => {
39
+ let safe = value;
40
+ for (const secret of secrets.filter(Boolean))
41
+ safe = safe.split(secret).join("[REDACTED]");
42
+ return safe.replace(/Bearer\s+[^\s"']+/gi, "Bearer [REDACTED]");
43
+ };
44
+ }
45
+ /** Observer only: never returns modified payloads, tools, messages, or stream bytes. */
46
+ export class Instrumentation {
47
+ state = emptyObservation();
48
+ compacting = false;
49
+ update;
50
+ now;
51
+ redact;
52
+ constructor(update = () => { }, now = () => performance.now(), redact = (text) => text) {
53
+ this.update = update;
54
+ this.now = now;
55
+ this.redact = redact;
56
+ }
57
+ systemPrompt(prompt) {
58
+ this.state.systemPromptHash = hash(prompt);
59
+ this.publish();
60
+ }
61
+ publish() {
62
+ this.update(this.state);
63
+ }
64
+ onEvent(event) {
65
+ const at = this.now();
66
+ switch (event.type) {
67
+ case "agent_start":
68
+ this.state.agentStartedAtMs ??= at;
69
+ break;
70
+ case "agent_settled":
71
+ this.state.agentEndedAtMs = at;
72
+ break;
73
+ case "turn_start":
74
+ this.state.agentTurns++;
75
+ break;
76
+ case "compaction_start":
77
+ this.compacting = true;
78
+ this.state.compactions++;
79
+ break;
80
+ case "compaction_end":
81
+ this.compacting = false;
82
+ break;
83
+ case "auto_retry_start":
84
+ this.state.retries++;
85
+ break;
86
+ case "tool_execution_start":
87
+ this.state.tools.push({
88
+ id: event.toolCallId,
89
+ name: event.toolName,
90
+ startedAtMs: at,
91
+ endedAtMs: null,
92
+ isError: null,
93
+ error: null,
94
+ });
95
+ break;
96
+ case "tool_execution_end": {
97
+ const tool = this.state.tools.find((item) => item.id === event.toolCallId);
98
+ if (tool) {
99
+ tool.endedAtMs = at;
100
+ tool.isError = event.isError;
101
+ }
102
+ break;
103
+ }
104
+ case "message_end": {
105
+ const message = event.message;
106
+ if (message.role === "assistant") {
107
+ this.state.assistantMessages++;
108
+ this.state.lastAssistantStopReason = message.stopReason;
109
+ if (message.errorMessage)
110
+ this.state.errors.push(this.redact(message.errorMessage).slice(0, 8000));
111
+ for (const block of message.content)
112
+ if (block.type === "toolCall") {
113
+ this.state.toolCalls++;
114
+ this.state.toolCallsByType[block.name] =
115
+ (this.state.toolCallsByType[block.name] ?? 0) + 1;
116
+ }
117
+ }
118
+ else if (message.role === "toolResult") {
119
+ let tool = this.state.tools.find((item) => item.id === message.toolCallId);
120
+ if (!tool) {
121
+ tool = {
122
+ id: message.toolCallId,
123
+ name: message.toolName,
124
+ startedAtMs: null,
125
+ endedAtMs: at,
126
+ isError: message.isError,
127
+ error: null,
128
+ };
129
+ this.state.tools.push(tool);
130
+ }
131
+ tool.isError = message.isError;
132
+ if (message.isError)
133
+ tool.error = this.redact(message.content
134
+ .filter((item) => item.type === "text")
135
+ .map((item) => item.text)
136
+ .join("\n")).slice(0, 4000);
137
+ }
138
+ break;
139
+ }
140
+ default:
141
+ return;
142
+ }
143
+ this.publish();
144
+ }
145
+ observeFetch(fetcher) {
146
+ return async (input, init) => {
147
+ const url = new URL(input instanceof Request ? input.url : input.toString());
148
+ if (!url.pathname.endsWith("/chat/completions"))
149
+ return fetcher(input, init);
150
+ const trace = {
151
+ request: this.state.requests.length + 1,
152
+ purpose: this.compacting ? "compaction" : "agent",
153
+ startedAtMs: this.now(),
154
+ endedAtMs: null,
155
+ firstContentAtMs: null,
156
+ status: null,
157
+ requestId: null,
158
+ servedModel: null,
159
+ systemFingerprint: null,
160
+ finishReason: null,
161
+ usage: null,
162
+ error: null,
163
+ streamComplete: false,
164
+ };
165
+ this.state.requests.push(trace);
166
+ this.publish();
167
+ try {
168
+ const response = await fetcher(input, init);
169
+ trace.status = response.status;
170
+ trace.requestId = response.headers.get("x-request-id");
171
+ this.publish();
172
+ if (!response.body) {
173
+ trace.endedAtMs = this.now();
174
+ this.publish();
175
+ return response;
176
+ }
177
+ const decoder = new TextDecoder();
178
+ let buffer = "";
179
+ let dropped = false;
180
+ const consume = (data) => {
181
+ if (data.trim() === "[DONE]") {
182
+ trace.streamComplete = true;
183
+ return;
184
+ }
185
+ let chunk;
186
+ try {
187
+ chunk = JSON.parse(data);
188
+ }
189
+ catch {
190
+ return;
191
+ }
192
+ if (!isRecord(chunk))
193
+ return;
194
+ if (typeof chunk.model === "string")
195
+ trace.servedModel = chunk.model;
196
+ if (typeof chunk.system_fingerprint === "string")
197
+ trace.systemFingerprint = chunk.system_fingerprint;
198
+ if (chunk.usage)
199
+ trace.usage = reportedUsage(chunk.usage);
200
+ for (const choice of Array.isArray(chunk.choices) ? chunk.choices : []) {
201
+ if (!isRecord(choice))
202
+ continue;
203
+ if (typeof choice.finish_reason === "string")
204
+ trace.finishReason = choice.finish_reason;
205
+ if (choice.usage)
206
+ trace.usage = reportedUsage(choice.usage);
207
+ const delta = isRecord(choice.delta) ? choice.delta : {};
208
+ if (trace.firstContentAtMs === null &&
209
+ ([delta.content, delta.reasoning_content, delta.reasoning].some((value) => typeof value === "string" && value.length > 0) ||
210
+ (Array.isArray(delta.tool_calls) &&
211
+ delta.tool_calls.some((call) => {
212
+ if (!isRecord(call) || !isRecord(call.function))
213
+ return false;
214
+ return [call.function.name, call.function.arguments].some((value) => typeof value === "string" && value.length > 0);
215
+ }))))
216
+ trace.firstContentAtMs = this.now();
217
+ }
218
+ };
219
+ const ingest = (bytes) => {
220
+ if (dropped)
221
+ return;
222
+ const previous = JSON.stringify([trace.usage, trace.firstContentAtMs]);
223
+ buffer += decoder.decode(bytes, { stream: true });
224
+ if (buffer.length > 1024 * 1024) {
225
+ dropped = true;
226
+ buffer = "";
227
+ trace.error = "Instrumentation frame exceeded 1 MiB; usage may be incomplete";
228
+ return;
229
+ }
230
+ // Line parser handles arbitrary TCP chunking, LF and CRLF. JSON payloads
231
+ // are contained in SSE data lines for Nebius's Chat Completions protocol.
232
+ let newline = buffer.indexOf("\n");
233
+ while (newline >= 0) {
234
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
235
+ buffer = buffer.slice(newline + 1);
236
+ if (line.startsWith("data:"))
237
+ consume(line.slice(5).trimStart());
238
+ newline = buffer.indexOf("\n");
239
+ }
240
+ if (JSON.stringify([trace.usage, trace.firstContentAtMs]) !== previous)
241
+ this.publish();
242
+ };
243
+ const reader = response.body.getReader();
244
+ const finish = () => {
245
+ trace.endedAtMs = this.now();
246
+ this.publish();
247
+ };
248
+ const body = new ReadableStream({
249
+ pull: async (controller) => {
250
+ try {
251
+ const result = await reader.read();
252
+ if (result.done) {
253
+ buffer += decoder.decode();
254
+ if (buffer.startsWith("data:"))
255
+ consume(buffer.slice(5).trim());
256
+ if (!response.ok)
257
+ trace.error = this.redact(`HTTP ${response.status}: ${buffer}`).slice(0, 8000);
258
+ finish();
259
+ controller.close();
260
+ return;
261
+ }
262
+ // Forward exactly the original bytes. Observation is best effort and cannot fail inference.
263
+ try {
264
+ ingest(result.value);
265
+ }
266
+ catch {
267
+ trace.error = "Instrumentation could not decode response metadata";
268
+ }
269
+ controller.enqueue(result.value);
270
+ }
271
+ catch (error) {
272
+ trace.error = this.redact(String(error)).slice(0, 8000);
273
+ finish();
274
+ controller.error(error);
275
+ }
276
+ },
277
+ cancel: async (reason) => {
278
+ finish();
279
+ await reader.cancel(reason);
280
+ },
281
+ });
282
+ return new Response(body, {
283
+ status: response.status,
284
+ statusText: response.statusText,
285
+ headers: response.headers,
286
+ });
287
+ }
288
+ catch (error) {
289
+ trace.error = this.redact(String(error)).slice(0, 8000);
290
+ trace.endedAtMs = this.now();
291
+ this.publish();
292
+ throw error;
293
+ }
294
+ };
295
+ }
296
+ }
@@ -0,0 +1,78 @@
1
+ export function tokenTotals(requests) {
2
+ const sum = (field) => requests.reduce((total, request) => total + (request.usage?.[field] ?? 0), 0);
3
+ const complete = (field) => requests.length > 0 && requests.every((request) => request.usage?.[field] != null);
4
+ const input = complete("inputTokens") ? sum("inputTokens") : null;
5
+ const last = requests.filter((request) => request.purpose === "agent").at(-1)?.usage?.inputTokens ?? null;
6
+ return {
7
+ cumulativeInputTokens: input,
8
+ cumulativeOutputTokens: complete("outputTokens") ? sum("outputTokens") : null,
9
+ cachedInputTokens: complete("cachedInputTokens") ? sum("cachedInputTokens") : null,
10
+ reasoningTokens: complete("reasoningTokens") ? sum("reasoningTokens") : null,
11
+ observedInputTokens: sum("inputTokens"),
12
+ observedOutputTokens: sum("outputTokens"),
13
+ requestsWithUsage: requests.filter((request) => request.usage?.inputTokens != null && request.usage.outputTokens != null).length,
14
+ usageComplete: complete("inputTokens") && complete("outputTokens"),
15
+ lastRequestInputTokens: last,
16
+ finalContextSizeTokens: null,
17
+ inputAmplification: null,
18
+ inputAmplificationVsLastRequest: input !== null && last !== null && last > 0 ? input / last : null,
19
+ };
20
+ }
21
+ export function distribution(values) {
22
+ const known = values
23
+ .filter((value) => value !== null && Number.isFinite(value))
24
+ .sort((a, b) => a - b);
25
+ const count = known.length;
26
+ if (!count)
27
+ return {
28
+ count: 0,
29
+ missing: values.length,
30
+ mean: null,
31
+ median: null,
32
+ min: null,
33
+ max: null,
34
+ standardDeviation: null,
35
+ };
36
+ const mean = known.reduce((a, b) => a + b, 0) / count;
37
+ return {
38
+ count,
39
+ missing: values.length - count,
40
+ mean,
41
+ median: ((known[Math.floor((count - 1) / 2)] ?? 0) + (known[Math.floor(count / 2)] ?? 0)) / 2,
42
+ min: known[0] ?? null,
43
+ max: known.at(-1) ?? null,
44
+ standardDeviation: Math.sqrt(known.reduce((total, value) => total + (value - mean) ** 2, 0) / count),
45
+ };
46
+ }
47
+ export function aggregate(runs) {
48
+ return [...new Set(runs.map((run) => run.model))].map((model) => {
49
+ const group = runs.filter((run) => run.model === model);
50
+ const successes = group.filter((run) => run.success).length;
51
+ return {
52
+ model,
53
+ runs: group.length,
54
+ successes,
55
+ successRate: group.every((run) => run.validation?.checked === false)
56
+ ? null
57
+ : successes / group.length,
58
+ wallTimeMs: distribution(group.map((run) => run.wallTimeMs)),
59
+ inputTokens: distribution(group.map((run) => run.tokens.cumulativeInputTokens)),
60
+ outputTokens: distribution(group.map((run) => run.tokens.cumulativeOutputTokens)),
61
+ turns: distribution(group.map((run) => run.agentTurns)),
62
+ toolCalls: distribution(group.map((run) => run.toolCalls)),
63
+ };
64
+ });
65
+ }
66
+ /** Union, rather than sum, avoids double-counting parallel tool/request intervals. */
67
+ export function intervalDuration(intervals) {
68
+ const ordered = intervals
69
+ .filter((item) => item.startedAtMs !== null && item.endedAtMs !== null)
70
+ .sort((a, b) => a.startedAtMs - b.startedAtMs);
71
+ let total = 0;
72
+ let end = 0;
73
+ for (const item of ordered) {
74
+ total += Math.max(0, item.endedAtMs - Math.max(end, item.startedAtMs));
75
+ end = Math.max(end, item.endedAtMs);
76
+ }
77
+ return total;
78
+ }
@@ -0,0 +1,122 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ export function cleanEnvironment() {
4
+ return { PATH: process.env.PATH, LANG: "C.UTF-8", LC_ALL: "C.UTF-8", TZ: "UTC" };
5
+ }
6
+ export function terminateGroup(pid, signal = "SIGTERM") {
7
+ try {
8
+ process.kill(-pid, signal);
9
+ }
10
+ catch {
11
+ try {
12
+ process.kill(pid, signal);
13
+ }
14
+ catch {
15
+ /* Already exited. */
16
+ }
17
+ }
18
+ }
19
+ export async function descendants(pid) {
20
+ try {
21
+ const { stdout } = await promisify(execFile)("ps", ["-axo", "pid=,ppid="], {
22
+ maxBuffer: 4 * 1024 * 1024,
23
+ });
24
+ const rows = stdout
25
+ .trim()
26
+ .split("\n")
27
+ .map((line) => line.trim().split(/\s+/).map(Number));
28
+ const found = new Set([pid]);
29
+ for (let changed = true; changed;) {
30
+ changed = false;
31
+ for (const [child, parent] of rows)
32
+ if (child && parent && found.has(parent) && !found.has(child)) {
33
+ found.add(child);
34
+ changed = true;
35
+ }
36
+ }
37
+ found.delete(pid);
38
+ return [...found].reverse();
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ export async function runCommand(command, cwd, timeoutMs, signal, redact = (text) => text) {
45
+ const started = performance.now();
46
+ if (signal?.aborted)
47
+ return {
48
+ command,
49
+ exitCode: null,
50
+ signal: "SIGINT",
51
+ durationMs: 0,
52
+ timedOut: false,
53
+ output: "Cancelled before validation",
54
+ outputTruncated: false,
55
+ };
56
+ return new Promise((resolve) => {
57
+ const child = spawn(command.command, command.args, {
58
+ cwd,
59
+ env: cleanEnvironment(),
60
+ detached: true,
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ });
63
+ const chunks = [];
64
+ let bytes = 0;
65
+ let truncated = false;
66
+ let timedOut = false;
67
+ let settled = false;
68
+ let force;
69
+ const stop = () => {
70
+ if (!child.pid)
71
+ return;
72
+ terminateGroup(child.pid);
73
+ force ??= setTimeout(() => {
74
+ if (child.pid)
75
+ terminateGroup(child.pid, "SIGKILL");
76
+ }, 1000);
77
+ };
78
+ const timer = setTimeout(() => {
79
+ timedOut = true;
80
+ stop();
81
+ }, timeoutMs);
82
+ signal?.addEventListener("abort", stop, { once: true });
83
+ const capture = (data) => {
84
+ const remaining = 256 * 1024 - bytes;
85
+ if (data.length > remaining)
86
+ truncated = true;
87
+ if (remaining > 0) {
88
+ chunks.push(data.subarray(0, remaining));
89
+ bytes += Math.min(remaining, data.length);
90
+ }
91
+ };
92
+ child.stdout.on("data", capture);
93
+ child.stderr.on("data", capture);
94
+ const finish = (code, exitSignal, error) => {
95
+ if (settled)
96
+ return;
97
+ settled = true;
98
+ clearTimeout(timer);
99
+ if (force)
100
+ clearTimeout(force);
101
+ signal?.removeEventListener("abort", stop);
102
+ // Remove descendants that kept stdio open after their command exited.
103
+ if (child.pid)
104
+ terminateGroup(child.pid, "SIGKILL");
105
+ resolve({
106
+ command,
107
+ exitCode: code,
108
+ signal: exitSignal,
109
+ durationMs: performance.now() - started,
110
+ timedOut,
111
+ output: redact(error ?? Buffer.concat(chunks).toString("utf8")),
112
+ outputTruncated: truncated,
113
+ });
114
+ };
115
+ child.on("error", (error) => finish(null, null, String(error)));
116
+ child.on("exit", (code, exitSignal) => {
117
+ if (child.pid)
118
+ terminateGroup(child.pid, "SIGKILL");
119
+ child.once("close", () => finish(code, exitSignal));
120
+ });
121
+ });
122
+ }
@@ -0,0 +1,70 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { cp, lstat, mkdir, readdir } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ const excluded = (path) => path
5
+ .split(/[\\/]/)
6
+ .some((name) => [
7
+ ".git",
8
+ ".pi",
9
+ "node_modules",
10
+ "benchmark-results",
11
+ "dist",
12
+ "build",
13
+ "coverage",
14
+ ".next",
15
+ ].includes(name) ||
16
+ name === ".env" ||
17
+ name.startsWith(".env.") ||
18
+ /\.(pem|key)$/.test(name));
19
+ /** Snapshot working files, respecting Git ignores, without dependency trees or known credential files. */
20
+ export async function snapshotProject(source, target) {
21
+ let files;
22
+ try {
23
+ files = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
24
+ cwd: source,
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ maxBuffer: 8 * 1024 * 1024,
28
+ })
29
+ .split("\0")
30
+ .filter(Boolean);
31
+ }
32
+ catch {
33
+ const walk = async (prefix = "") => {
34
+ const found = [];
35
+ for (const entry of await readdir(join(source, prefix), { withFileTypes: true })) {
36
+ const path = join(prefix, entry.name);
37
+ if (excluded(path))
38
+ continue;
39
+ if (entry.isDirectory())
40
+ found.push(...(await walk(path)));
41
+ else
42
+ found.push(path);
43
+ }
44
+ return found;
45
+ };
46
+ files = await walk();
47
+ }
48
+ await mkdir(target, { recursive: true, mode: 0o700 });
49
+ let bytes = 0;
50
+ let copied = 0;
51
+ for (const path of new Set(files)) {
52
+ if (excluded(path))
53
+ continue;
54
+ const info = await lstat(join(source, path)).catch((error) => {
55
+ if (error.code === "ENOENT")
56
+ return undefined;
57
+ throw error;
58
+ });
59
+ if (!info)
60
+ continue;
61
+ if (!info.isFile())
62
+ throw new Error(`Cannot snapshot ${path}: only regular project files are supported.`);
63
+ bytes += info.size;
64
+ if (++copied > 10000 || bytes > 50 * 1024 * 1024)
65
+ throw new Error("Project snapshot exceeds 10,000 files or 50 MiB. Run Pi from a smaller project directory.");
66
+ await mkdir(dirname(join(target, path)), { recursive: true, mode: 0o700 });
67
+ await cp(join(source, path), join(target, path));
68
+ }
69
+ return copied;
70
+ }
@@ -0,0 +1,94 @@
1
+ import { distribution } from "./metrics.js";
2
+ /** Client-observed first generated content on the first agent request, not worker startup. */
3
+ export function observedTtftMs(run) {
4
+ const first = run.observation.requests.find((request) => request.purpose === "agent");
5
+ if (first?.firstContentAtMs == null)
6
+ return null;
7
+ const elapsed = first.firstContentAtMs - first.startedAtMs;
8
+ return elapsed >= 0 ? elapsed : null;
9
+ }
10
+ /** End-to-end output rate, including tools, network, setup, and validation. */
11
+ export function outputThroughput(runs) {
12
+ if (!runs.length ||
13
+ runs.some((run) => run.tokens.cumulativeOutputTokens === null || run.wallTimeMs <= 0))
14
+ return null;
15
+ const tokens = runs.reduce((sum, run) => sum + (run.tokens.cumulativeOutputTokens ?? 0), 0);
16
+ return tokens / (runs.reduce((sum, run) => sum + run.wallTimeMs, 0) / 1000);
17
+ }
18
+ function failureDetails(run) {
19
+ const failed = run.validation.commands.find((command) => command.exitCode !== 0 || command.timedOut);
20
+ const output = failed?.output ?? "";
21
+ const reason = output.match(/\berror: \|-\n\s+([^\n]+)/)?.[1] ?? output.match(/\berror: (.+)/)?.[1];
22
+ const named = output.match(/not ok \d+ - (.+)/)?.[1];
23
+ const location = output.match(/TestContext[^\n]*\/([^/\n]+\.(?:mjs|js|ts):\d+:\d+)/)?.[1];
24
+ const detail = reason && reason !== "|-" ? reason : named;
25
+ return [run.errors[0] || detail, location]
26
+ .filter(Boolean)
27
+ .join("; ")
28
+ .replace(/\s+/g, " ")
29
+ .slice(0, 350);
30
+ }
31
+ export function terminalReport(results) {
32
+ const unchecked = results.definition.validationMode === "none";
33
+ const groups = results.aggregates;
34
+ const runs = groups.map((group) => results.runs.filter((run) => run.model === group.model));
35
+ const number = (value, decimals = 1) => value === null
36
+ ? "n/a"
37
+ : value.toLocaleString("en-US", {
38
+ minimumFractionDigits: decimals,
39
+ maximumFractionDigits: decimals,
40
+ });
41
+ const seconds = (value) => value === null ? "n/a" : `${number(value / 1000, 2)} s`;
42
+ const rows = [
43
+ ["Metric", ...groups.map((group) => group.model)],
44
+ [
45
+ unchecked ? "Finished runs (not validated)" : "Validated success",
46
+ ...groups.map((group, index) => `${unchecked ? runs[index]?.filter((run) => run.failure === null).length : group.successes}/${group.runs}`),
47
+ ],
48
+ ["Input tokens / run (mean)", ...groups.map((group) => number(group.inputTokens.mean, 0))],
49
+ ["Output tokens / run (mean)", ...groups.map((group) => number(group.outputTokens.mean, 0))],
50
+ ["Task duration (median)", ...groups.map((group) => seconds(group.wallTimeMs.median))],
51
+ [
52
+ "Observed TTFT (median)",
53
+ ...runs.map((group) => seconds(distribution(group.map(observedTtftMs)).median)),
54
+ ],
55
+ ["Throughput (output tokens/s)", ...runs.map((group) => number(outputThroughput(group)))],
56
+ ["Agent turns / run (mean)", ...groups.map((group) => number(group.turns.mean))],
57
+ ["Tool calls / run (mean)", ...groups.map((group) => number(group.toolCalls.mean))],
58
+ ];
59
+ const widths = rows[0]?.map((_, index) => Math.max(...rows.map((row) => row[index]?.length ?? 0))) ?? [];
60
+ const separator = widths.map((width) => "─".repeat(width)).join("─┼─");
61
+ const table = rows.map((row) => row
62
+ .map((cell, index) => cell.padEnd(widths[index] ?? 0))
63
+ .join(" │ ")
64
+ .trimEnd());
65
+ table.splice(1, 0, separator);
66
+ const failures = results.runs.filter((run) => run.failure !== null);
67
+ return [
68
+ `Benchmark: ${results.definition.name}`,
69
+ `Completed: ${results.runs.length}/${results.plannedRuns} (${results.status})`,
70
+ ...(unchecked
71
+ ? ["Correctness: not checked. Finished means execution completed without an error."]
72
+ : []),
73
+ "",
74
+ ...table,
75
+ "",
76
+ "Tokens include every request in a run. Failed runs contribute to these metrics too.",
77
+ "TTFT: first agent request start → first streamed text/reasoning/tool content; excludes worker startup.",
78
+ "Stream chunks may contain multiple tokens; TTFT is client-observed, not server token timing.",
79
+ "Throughput: total output tokens ÷ total task seconds, including network, tools, and validation.",
80
+ "Unknown values are n/a. Means/medians exclude missing values; throughput needs complete output usage for every run.",
81
+ ...(failures.length
82
+ ? [
83
+ "",
84
+ "Failed runs:",
85
+ ...failures.map((run) => {
86
+ const details = failureDetails(run);
87
+ return `- ${run.model} #${run.run}: ${run.failure}${details ? ` — ${details}` : ""}`;
88
+ }),
89
+ ]
90
+ : []),
91
+ "",
92
+ "Detailed per-run measurements and validator output: results.json.",
93
+ ].join("\n");
94
+ }