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.
- package/CHANGELOG.md +49 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/SECURITY.md +47 -0
- package/benchmarks/add-api-endpoint/benchmark.yaml +13 -0
- package/benchmarks/add-api-endpoint/fixture/app.mjs +6 -0
- package/benchmarks/add-api-endpoint/fixture/app.test.mjs +8 -0
- package/benchmarks/add-api-endpoint/fixture/package.json +8 -0
- package/benchmarks/add-api-endpoint/validation/check.test.mjs +37 -0
- package/benchmarks/fix-auth-bug/benchmark.yaml +14 -0
- package/benchmarks/fix-auth-bug/fixture/auth.mjs +4 -0
- package/benchmarks/fix-auth-bug/fixture/auth.test.mjs +26 -0
- package/benchmarks/fix-auth-bug/fixture/package.json +8 -0
- package/benchmarks/fix-auth-bug/validation/check.test.mjs +25 -0
- package/benchmarks/multi-file-feature/benchmark.yaml +15 -0
- package/benchmarks/multi-file-feature/fixture/package.json +8 -0
- package/benchmarks/multi-file-feature/fixture/routes.mjs +11 -0
- package/benchmarks/multi-file-feature/fixture/routes.test.mjs +14 -0
- package/benchmarks/multi-file-feature/fixture/serialize.mjs +3 -0
- package/benchmarks/multi-file-feature/fixture/store.mjs +10 -0
- package/benchmarks/multi-file-feature/validation/check.test.mjs +52 -0
- package/benchmarks/refactor-module/benchmark.yaml +12 -0
- package/benchmarks/refactor-module/fixture/invoice.mjs +8 -0
- package/benchmarks/refactor-module/fixture/invoice.test.mjs +9 -0
- package/benchmarks/refactor-module/fixture/package.json +8 -0
- package/benchmarks/refactor-module/validation/check.test.mjs +36 -0
- package/dist/benchmark/cli.js +112 -0
- package/dist/benchmark/command.js +194 -0
- package/dist/benchmark/definition.js +109 -0
- package/dist/benchmark/host-worker.js +14 -0
- package/dist/benchmark/instrumentation.js +296 -0
- package/dist/benchmark/metrics.js +78 -0
- package/dist/benchmark/process.js +122 -0
- package/dist/benchmark/project.js +70 -0
- package/dist/benchmark/report.js +94 -0
- package/dist/benchmark/runner.js +376 -0
- package/dist/benchmark/types.js +1 -0
- package/dist/benchmark/worker.js +134 -0
- package/dist/benchmark/workspace.js +55 -0
- package/dist/discovery.js +154 -0
- package/dist/errors.js +32 -0
- package/dist/index.js +86 -0
- package/dist/model-settings-command.js +130 -0
- package/dist/model-settings.js +101 -0
- package/dist/models.js +62 -0
- package/dist/provider.js +48 -0
- package/docs/benchmark-research.md +35 -0
- package/docs/benchmarking.md +253 -0
- package/docs/security-review.md +49 -0
- package/docs/validation.md +51 -0
- package/examples/models.json +31 -0
- package/package.json +74 -0
- package/src/benchmark/cli.ts +118 -0
- package/src/benchmark/command.ts +218 -0
- package/src/benchmark/definition.ts +112 -0
- package/src/benchmark/host-worker.ts +14 -0
- package/src/benchmark/instrumentation.ts +298 -0
- package/src/benchmark/metrics.ts +101 -0
- package/src/benchmark/process.ts +120 -0
- package/src/benchmark/project.ts +71 -0
- package/src/benchmark/report.ts +111 -0
- package/src/benchmark/runner.ts +458 -0
- package/src/benchmark/types.ts +180 -0
- package/src/benchmark/worker.ts +150 -0
- package/src/benchmark/workspace.ts +64 -0
- package/src/discovery.ts +176 -0
- package/src/errors.ts +32 -0
- package/src/index.ts +96 -0
- package/src/model-settings-command.ts +151 -0
- package/src/model-settings.ts +129 -0
- package/src/models.ts +73 -0
- package/src/provider.ts +63 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { arch, platform, release } from "node:os";
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import type { ModelSettingsMap } from "../model-settings.ts";
|
|
9
|
+
import type { NebiusModel } from "../models.ts";
|
|
10
|
+
import { emptyObservation, hash, redactor } from "./instrumentation.ts";
|
|
11
|
+
import { aggregate, intervalDuration, tokenTotals } from "./metrics.ts";
|
|
12
|
+
import { cleanEnvironment, descendants, runCommand, terminateGroup } from "./process.ts";
|
|
13
|
+
import type {
|
|
14
|
+
BenchmarkDefinition,
|
|
15
|
+
Command,
|
|
16
|
+
Failure,
|
|
17
|
+
Observation,
|
|
18
|
+
Results,
|
|
19
|
+
RunResult,
|
|
20
|
+
WorkerInput,
|
|
21
|
+
WorkerMessage,
|
|
22
|
+
} from "./types.ts";
|
|
23
|
+
import { copyTree, hashTree } from "./workspace.ts";
|
|
24
|
+
|
|
25
|
+
export interface RunnerOptions {
|
|
26
|
+
definition: BenchmarkDefinition;
|
|
27
|
+
directory: string;
|
|
28
|
+
models: NebiusModel[];
|
|
29
|
+
modelSettings?: ModelSettingsMap;
|
|
30
|
+
runs: number;
|
|
31
|
+
concurrency: number;
|
|
32
|
+
output: string;
|
|
33
|
+
apiKey: string;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
onRun?: (result: RunResult) => void;
|
|
36
|
+
/** Injectable worker only for integration tests/embedding; not exposed by the CLI. */
|
|
37
|
+
workerPath?: string;
|
|
38
|
+
workerArgs?: string[];
|
|
39
|
+
/** Host Pi entry when running from a production-only extension installation. */
|
|
40
|
+
piEntry?: string;
|
|
41
|
+
}
|
|
42
|
+
export function classifyFailure(input: {
|
|
43
|
+
timedOut?: boolean;
|
|
44
|
+
cancelled?: boolean;
|
|
45
|
+
error?: string | null;
|
|
46
|
+
requests?: Observation["requests"];
|
|
47
|
+
validated?: boolean;
|
|
48
|
+
toolErrors?: number;
|
|
49
|
+
}): Failure | null {
|
|
50
|
+
if (input.cancelled) return "cancelled";
|
|
51
|
+
if (input.timedOut) return "timeout";
|
|
52
|
+
const error = input.error ?? "";
|
|
53
|
+
if (/context[_ ](?:length|limit)|maximum context|too many tokens|prompt is too long/i.test(error))
|
|
54
|
+
return "context_limit";
|
|
55
|
+
if (/\b429\b|rate.?limit/i.test(error)) return "rate_limit";
|
|
56
|
+
if (error) {
|
|
57
|
+
if (/\b(?:4\d\d|5\d\d)\b|API|connection|fetch|network/i.test(error)) return "model_api_error";
|
|
58
|
+
return "agent_error";
|
|
59
|
+
}
|
|
60
|
+
if (input.validated === false) return input.toolErrors ? "tool_error" : "validation_failed";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
async function atomicJson(path: string, value: unknown, redact: (text: string) => string) {
|
|
64
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
65
|
+
await writeFile(temporary, `${redact(JSON.stringify(value, null, 2))}\n`, { mode: 0o600 });
|
|
66
|
+
await rename(temporary, path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function executeAgent(
|
|
70
|
+
input: WorkerInput,
|
|
71
|
+
cwd: string,
|
|
72
|
+
journal: string,
|
|
73
|
+
signal: AbortSignal | undefined,
|
|
74
|
+
workerPath: string | undefined,
|
|
75
|
+
workerArgs: string[] | undefined,
|
|
76
|
+
redact: (text: string) => string,
|
|
77
|
+
) {
|
|
78
|
+
let observation = emptyObservation();
|
|
79
|
+
let effectiveSettings: Record<string, unknown> = {};
|
|
80
|
+
let error: string | null = null;
|
|
81
|
+
let timedOut = false;
|
|
82
|
+
let done = false;
|
|
83
|
+
let stopped = false;
|
|
84
|
+
const started = performance.now();
|
|
85
|
+
const worker = workerPath ?? fileURLToPath(new URL("./worker.js", import.meta.url));
|
|
86
|
+
const child = fork(worker, workerArgs ?? [], {
|
|
87
|
+
cwd,
|
|
88
|
+
env: { ...cleanEnvironment(), PI_CODING_AGENT_DIR: input.agentDir },
|
|
89
|
+
execArgv: [],
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
92
|
+
});
|
|
93
|
+
let stderr = "";
|
|
94
|
+
child.stderr?.on("data", (data) => {
|
|
95
|
+
stderr = (stderr + data).slice(-8000);
|
|
96
|
+
});
|
|
97
|
+
let force: NodeJS.Timeout | undefined;
|
|
98
|
+
let descendantPids: number[] = [];
|
|
99
|
+
const stop = () => {
|
|
100
|
+
if (stopped) return;
|
|
101
|
+
stopped = true;
|
|
102
|
+
if (child.pid)
|
|
103
|
+
void descendants(child.pid).then((pids) => {
|
|
104
|
+
descendantPids = pids;
|
|
105
|
+
});
|
|
106
|
+
if (child.connected) child.send("abort", () => {});
|
|
107
|
+
force = setTimeout(() => {
|
|
108
|
+
for (const pid of descendantPids) terminateGroup(pid, "SIGKILL");
|
|
109
|
+
if (child.pid) terminateGroup(child.pid, "SIGKILL");
|
|
110
|
+
}, 2000);
|
|
111
|
+
};
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
timedOut = true;
|
|
114
|
+
stop();
|
|
115
|
+
}, input.definition.timeout * 1000);
|
|
116
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
117
|
+
await new Promise<void>((resolveExit) => {
|
|
118
|
+
child.on("message", (message: WorkerMessage) => {
|
|
119
|
+
if (message.type === "observation") {
|
|
120
|
+
observation = message.observation;
|
|
121
|
+
try {
|
|
122
|
+
appendFileSync(journal, `${redact(JSON.stringify({ schemaVersion: 2, ...message }))}\n`, {
|
|
123
|
+
mode: 0o600,
|
|
124
|
+
});
|
|
125
|
+
} catch (caught) {
|
|
126
|
+
error = `Trace persistence failed: ${caught}`;
|
|
127
|
+
stop();
|
|
128
|
+
}
|
|
129
|
+
} else if (message.type === "settings") {
|
|
130
|
+
effectiveSettings = message.effectiveSettings;
|
|
131
|
+
} else if (message.type === "done") {
|
|
132
|
+
done = true;
|
|
133
|
+
error ??= message.error;
|
|
134
|
+
effectiveSettings = message.effectiveSettings;
|
|
135
|
+
// Pi's shell tools may start detached descendants. Clean those while the
|
|
136
|
+
// worker is still alive, before it can orphan them.
|
|
137
|
+
if (child.pid)
|
|
138
|
+
void descendants(child.pid).then((pids) => {
|
|
139
|
+
for (const pid of pids) terminateGroup(pid, "SIGKILL");
|
|
140
|
+
if (child.connected) child.send("shutdown", () => {});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
child.on("error", (caught) => {
|
|
145
|
+
error = String(caught);
|
|
146
|
+
resolveExit();
|
|
147
|
+
});
|
|
148
|
+
child.on("exit", (code, exitSignal) => {
|
|
149
|
+
if (!done && !timedOut && !signal?.aborted)
|
|
150
|
+
error ??= `Pi worker exited (${code ?? exitSignal}): ${stderr}`;
|
|
151
|
+
resolveExit();
|
|
152
|
+
});
|
|
153
|
+
child.send(input, (caught) => {
|
|
154
|
+
if (caught) {
|
|
155
|
+
error = String(caught);
|
|
156
|
+
stop();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (signal?.aborted) stop();
|
|
160
|
+
});
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
if (force) clearTimeout(force);
|
|
163
|
+
signal?.removeEventListener("abort", stop);
|
|
164
|
+
for (const pid of descendantPids) terminateGroup(pid, "SIGKILL");
|
|
165
|
+
return {
|
|
166
|
+
observation,
|
|
167
|
+
effectiveSettings,
|
|
168
|
+
error: error ? redact(error) : null,
|
|
169
|
+
timedOut,
|
|
170
|
+
durationMs: performance.now() - started,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function expand(command: Command, validationDirectory: string): Command {
|
|
175
|
+
return {
|
|
176
|
+
command: command.command.replaceAll("{validation}", validationDirectory),
|
|
177
|
+
args: command.args.map((arg) => arg.replaceAll("{validation}", validationDirectory)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function executeRun(
|
|
182
|
+
options: RunnerOptions,
|
|
183
|
+
model: NebiusModel,
|
|
184
|
+
run: number,
|
|
185
|
+
output: string,
|
|
186
|
+
snapshot: string,
|
|
187
|
+
validationSnapshot: string,
|
|
188
|
+
fixtureHash: string,
|
|
189
|
+
validationHash: string,
|
|
190
|
+
): Promise<RunResult> {
|
|
191
|
+
const start = performance.now();
|
|
192
|
+
const id = `${String(run).padStart(3, "0")}-${hash(model.id).slice(0, 12)}`;
|
|
193
|
+
const directory = join(output, "runs", id);
|
|
194
|
+
const workspace = join(output, "active-workspace");
|
|
195
|
+
const archivedWorkspace = join(directory, "workspace");
|
|
196
|
+
const agentDir = join(directory, "pi");
|
|
197
|
+
const validationDirectory = join(directory, "validation");
|
|
198
|
+
const redact = redactor([options.apiKey]);
|
|
199
|
+
const result: RunResult = {
|
|
200
|
+
schemaVersion: 2,
|
|
201
|
+
id,
|
|
202
|
+
benchmark: options.definition.name,
|
|
203
|
+
model: model.id,
|
|
204
|
+
modelRevision: null,
|
|
205
|
+
run,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
success: false,
|
|
208
|
+
failure: null,
|
|
209
|
+
errors: [],
|
|
210
|
+
wallTimeMs: 0,
|
|
211
|
+
agentWallTimeMs: 0,
|
|
212
|
+
modelRequestWallTimeMs: 0,
|
|
213
|
+
toolExecutionTimeMs: 0,
|
|
214
|
+
timeToFirstContentMs: null,
|
|
215
|
+
modelGenerationTimeMs: null,
|
|
216
|
+
agentOverheadTimeMs: null,
|
|
217
|
+
agentTurns: 0,
|
|
218
|
+
modelRequests: 0,
|
|
219
|
+
toolCalls: 0,
|
|
220
|
+
toolErrors: 0,
|
|
221
|
+
toolCallsByType: {},
|
|
222
|
+
tokens: tokenTotals([]),
|
|
223
|
+
validation: {
|
|
224
|
+
checked: options.definition.validationMode !== "none",
|
|
225
|
+
passed: false,
|
|
226
|
+
exitCode: null,
|
|
227
|
+
durationMs: 0,
|
|
228
|
+
commands: [],
|
|
229
|
+
},
|
|
230
|
+
setup: [],
|
|
231
|
+
observation: emptyObservation(),
|
|
232
|
+
workspace: archivedWorkspace,
|
|
233
|
+
fixtureHash,
|
|
234
|
+
finalWorkspaceHash: null,
|
|
235
|
+
effectiveSettings: {},
|
|
236
|
+
};
|
|
237
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
238
|
+
try {
|
|
239
|
+
await mkdir(workspace, { mode: 0o700 });
|
|
240
|
+
await copyTree(snapshot, workspace);
|
|
241
|
+
if ((await hashTree(workspace)) !== fixtureHash)
|
|
242
|
+
throw new Error("Fixture snapshot changed; refusing contaminated run");
|
|
243
|
+
await mkdir(agentDir, { mode: 0o700 });
|
|
244
|
+
// Setup never sees credentials. Pin dependencies in the fixture if setup installs them.
|
|
245
|
+
for (const command of options.definition.setup) {
|
|
246
|
+
const setup = await runCommand(
|
|
247
|
+
command,
|
|
248
|
+
workspace,
|
|
249
|
+
options.definition.validationTimeout * 1000,
|
|
250
|
+
options.signal,
|
|
251
|
+
redact,
|
|
252
|
+
);
|
|
253
|
+
result.setup.push(setup);
|
|
254
|
+
if (setup.exitCode !== 0 || setup.timedOut)
|
|
255
|
+
throw new Error(`Benchmark setup failed: ${setup.output}`);
|
|
256
|
+
}
|
|
257
|
+
const executed = await executeAgent(
|
|
258
|
+
{
|
|
259
|
+
definition: options.definition,
|
|
260
|
+
model,
|
|
261
|
+
agentDir,
|
|
262
|
+
apiKey: options.apiKey,
|
|
263
|
+
modelSettings: options.modelSettings?.[model.id],
|
|
264
|
+
},
|
|
265
|
+
workspace,
|
|
266
|
+
join(directory, "trace.jsonl"),
|
|
267
|
+
options.signal,
|
|
268
|
+
options.workerPath,
|
|
269
|
+
options.workerArgs,
|
|
270
|
+
redact,
|
|
271
|
+
);
|
|
272
|
+
const observation = executed.observation;
|
|
273
|
+
result.observation = observation;
|
|
274
|
+
result.agentWallTimeMs = executed.durationMs;
|
|
275
|
+
result.effectiveSettings = executed.effectiveSettings;
|
|
276
|
+
result.agentTurns = observation.agentTurns;
|
|
277
|
+
result.modelRequests = observation.requests.length;
|
|
278
|
+
result.toolCalls = observation.toolCalls;
|
|
279
|
+
result.toolCallsByType = observation.toolCallsByType;
|
|
280
|
+
result.toolErrors = observation.tools.filter((tool) => tool.isError).length;
|
|
281
|
+
result.modelRequestWallTimeMs = observation.requests.some(
|
|
282
|
+
(request) => request.endedAtMs === null,
|
|
283
|
+
)
|
|
284
|
+
? null
|
|
285
|
+
: intervalDuration(observation.requests);
|
|
286
|
+
result.toolExecutionTimeMs = observation.tools.some(
|
|
287
|
+
(tool) => tool.startedAtMs !== null && tool.endedAtMs === null,
|
|
288
|
+
)
|
|
289
|
+
? null
|
|
290
|
+
: intervalDuration(observation.tools);
|
|
291
|
+
const first = observation.requests.find((request) => request.firstContentAtMs !== null);
|
|
292
|
+
result.timeToFirstContentMs =
|
|
293
|
+
first?.firstContentAtMs != null
|
|
294
|
+
? first.firstContentAtMs - (observation.agentStartedAtMs ?? first.startedAtMs)
|
|
295
|
+
: null;
|
|
296
|
+
result.tokens = tokenTotals(observation.requests);
|
|
297
|
+
result.errors = [...observation.errors, ...(executed.error ? [executed.error] : [])];
|
|
298
|
+
|
|
299
|
+
// Restore trusted validators AFTER the agent exits; edited fixture tests cannot replace these.
|
|
300
|
+
if ((await hashTree(validationSnapshot)) !== validationHash)
|
|
301
|
+
throw new Error("Trusted validation snapshot changed");
|
|
302
|
+
await copyTree(validationSnapshot, validationDirectory);
|
|
303
|
+
const validationStart = performance.now();
|
|
304
|
+
for (const command of options.definition.validation) {
|
|
305
|
+
result.validation.commands.push(
|
|
306
|
+
await runCommand(
|
|
307
|
+
expand(command, validationDirectory),
|
|
308
|
+
workspace,
|
|
309
|
+
options.definition.validationTimeout * 1000,
|
|
310
|
+
options.signal,
|
|
311
|
+
redact,
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
result.validation.durationMs = performance.now() - validationStart;
|
|
316
|
+
const passed =
|
|
317
|
+
result.validation.commands.length > 0 &&
|
|
318
|
+
result.validation.commands.every((command) => command.exitCode === 0 && !command.timedOut);
|
|
319
|
+
const integrity = (await hashTree(validationDirectory)) === validationHash;
|
|
320
|
+
if (!integrity) result.errors.push("Trusted validators were modified during validation");
|
|
321
|
+
result.validation.passed = passed && integrity;
|
|
322
|
+
result.validation.exitCode = result.validation.passed
|
|
323
|
+
? 0
|
|
324
|
+
: (result.validation.commands.find((command) => command.exitCode !== 0)?.exitCode ?? null);
|
|
325
|
+
const finalError =
|
|
326
|
+
executed.error ??
|
|
327
|
+
(["error", "aborted"].includes(observation.lastAssistantStopReason ?? "")
|
|
328
|
+
? (observation.errors.at(-1) ?? "Agent ended without completing")
|
|
329
|
+
: null);
|
|
330
|
+
result.failure = classifyFailure({
|
|
331
|
+
timedOut: executed.timedOut || result.validation.commands.some((command) => command.timedOut),
|
|
332
|
+
cancelled: options.signal?.aborted,
|
|
333
|
+
error: finalError,
|
|
334
|
+
validated: result.validation.checked ? result.validation.passed : undefined,
|
|
335
|
+
toolErrors: result.toolErrors,
|
|
336
|
+
});
|
|
337
|
+
result.success = result.failure === null && result.validation.passed;
|
|
338
|
+
try {
|
|
339
|
+
result.finalWorkspaceHash = await hashTree(workspace);
|
|
340
|
+
} catch {
|
|
341
|
+
result.errors.push("Final workspace hash unavailable (generated links or unsupported files)");
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
result.errors.push(redact(String(error)));
|
|
345
|
+
result.failure = options.signal?.aborted ? "cancelled" : "agent_error";
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
await copyTree(workspace, archivedWorkspace);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
result.errors.push(redact(`Workspace archival failed: ${error}`));
|
|
351
|
+
} finally {
|
|
352
|
+
await rm(workspace, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
result.wallTimeMs = performance.now() - start;
|
|
355
|
+
await atomicJson(join(directory, "run.json"), result, redact);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function runBenchmark(options: RunnerOptions): Promise<Results> {
|
|
360
|
+
if (options.concurrency !== 1)
|
|
361
|
+
throw new Error(
|
|
362
|
+
"v1 requires --concurrency 1 to preserve Pi's exact system prompt and isolate runs without rewriting cwd metadata",
|
|
363
|
+
);
|
|
364
|
+
if (process.platform === "win32")
|
|
365
|
+
throw new Error("v1 requires macOS or Linux for process-tree cancellation");
|
|
366
|
+
// Resolve existing ancestors before creating anything: a symlinked output
|
|
367
|
+
// parent must not smuggle the snapshot back inside its own source tree.
|
|
368
|
+
const canonical = async (path: string): Promise<string> => {
|
|
369
|
+
try {
|
|
370
|
+
return await realpath(path);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
373
|
+
const parent = dirname(path);
|
|
374
|
+
if (parent === path) throw error;
|
|
375
|
+
return resolve(await canonical(parent), relative(parent, path));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const output = await canonical(resolve(options.output));
|
|
379
|
+
for (const source of [options.definition.fixture, options.definition.validationDirectory]) {
|
|
380
|
+
const inside = relative(await realpath(resolve(options.directory, source)), output);
|
|
381
|
+
if (!isAbsolute(inside) && inside !== ".." && !inside.startsWith(`..${sep}`))
|
|
382
|
+
throw new Error("Output must be outside fixture and validation directories");
|
|
383
|
+
}
|
|
384
|
+
await mkdir(dirname(output), { recursive: true });
|
|
385
|
+
await mkdir(output, { mode: 0o700 }); // Never overwrite or mix result sets.
|
|
386
|
+
const redact = redactor([options.apiKey]);
|
|
387
|
+
const snapshot = join(output, "snapshot", "fixture");
|
|
388
|
+
const validationSnapshot = join(output, "snapshot", "validation");
|
|
389
|
+
await copyTree(join(options.directory, options.definition.fixture), snapshot);
|
|
390
|
+
await copyTree(
|
|
391
|
+
join(options.directory, options.definition.validationDirectory),
|
|
392
|
+
validationSnapshot,
|
|
393
|
+
);
|
|
394
|
+
const fixtureHash = await hashTree(snapshot);
|
|
395
|
+
const validationHash = await hashTree(validationSnapshot);
|
|
396
|
+
const packageJson = JSON.parse(
|
|
397
|
+
await readFile(new URL("../../package.json", import.meta.url), "utf8"),
|
|
398
|
+
);
|
|
399
|
+
// Resolve Pi's installed package through its public entry point, avoiding private exports.
|
|
400
|
+
const piEntry =
|
|
401
|
+
options.piEntry ?? fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
402
|
+
const piVersion = JSON.parse(
|
|
403
|
+
await readFile(join(dirname(piEntry), "../package.json"), "utf8"),
|
|
404
|
+
).version;
|
|
405
|
+
const results: Results = {
|
|
406
|
+
schemaVersion: 2,
|
|
407
|
+
status: "running",
|
|
408
|
+
timestamp: new Date().toISOString(),
|
|
409
|
+
metadata: {
|
|
410
|
+
piVersion,
|
|
411
|
+
piNebiusVersion: packageJson.version,
|
|
412
|
+
nodeVersion: process.version,
|
|
413
|
+
os: { platform: platform(), release: release(), architecture: arch() },
|
|
414
|
+
fixtureHash,
|
|
415
|
+
validationHash,
|
|
416
|
+
definitionHash: hash(JSON.stringify(options.definition)),
|
|
417
|
+
modelDefinitions: options.models.map(({ cost: _cost, ...model }) => model),
|
|
418
|
+
concurrency: options.concurrency,
|
|
419
|
+
runsPerModel: options.runs,
|
|
420
|
+
workerImplementation: options.workerPath ?? "built-in Pi SDK worker",
|
|
421
|
+
measurement: "provider-reported usage; client-observed wall times; no local token estimates",
|
|
422
|
+
},
|
|
423
|
+
definition: options.definition,
|
|
424
|
+
plannedRuns: options.runs * options.models.length,
|
|
425
|
+
runs: [],
|
|
426
|
+
aggregates: [],
|
|
427
|
+
};
|
|
428
|
+
await atomicJson(join(output, "results.json"), results, redact);
|
|
429
|
+
// Round-robin order avoids running every repetition of one model in one time window.
|
|
430
|
+
const jobs = Array.from({ length: options.runs }, (_, index) => index + 1).flatMap((run) =>
|
|
431
|
+
options.models.map((model) => ({ run, model })),
|
|
432
|
+
);
|
|
433
|
+
for (const job of jobs) {
|
|
434
|
+
if (options.signal?.aborted) break;
|
|
435
|
+
const result = await executeRun(
|
|
436
|
+
options,
|
|
437
|
+
job.model,
|
|
438
|
+
job.run,
|
|
439
|
+
output,
|
|
440
|
+
snapshot,
|
|
441
|
+
validationSnapshot,
|
|
442
|
+
fixtureHash,
|
|
443
|
+
validationHash,
|
|
444
|
+
);
|
|
445
|
+
results.runs.push(result);
|
|
446
|
+
results.aggregates = aggregate(results.runs);
|
|
447
|
+
options.onRun?.(result);
|
|
448
|
+
await atomicJson(join(output, "results.json"), results, redact);
|
|
449
|
+
}
|
|
450
|
+
results.runs.sort((a, b) => a.run - b.run || a.model.localeCompare(b.model));
|
|
451
|
+
results.status = options.signal?.aborted ? "cancelled" : "complete";
|
|
452
|
+
results.aggregates = aggregate(results.runs);
|
|
453
|
+
results.metadata.systemPromptHashes = [
|
|
454
|
+
...new Set(results.runs.map((run) => run.observation.systemPromptHash).filter(Boolean)),
|
|
455
|
+
];
|
|
456
|
+
await atomicJson(join(output, "results.json"), results, redact);
|
|
457
|
+
return results;
|
|
458
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { ModelSettings } from "../model-settings.ts";
|
|
2
|
+
import type { NebiusModel } from "../models.ts";
|
|
3
|
+
|
|
4
|
+
export interface Command {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface BenchmarkDefinition {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
name: string;
|
|
11
|
+
task: string;
|
|
12
|
+
fixture: string;
|
|
13
|
+
validationDirectory: string;
|
|
14
|
+
validation: Command[];
|
|
15
|
+
setup: Command[];
|
|
16
|
+
timeout: number;
|
|
17
|
+
validationTimeout: number;
|
|
18
|
+
tools: string[];
|
|
19
|
+
systemPrompt?: string;
|
|
20
|
+
validationMode?: "none";
|
|
21
|
+
}
|
|
22
|
+
export interface ReportedUsage {
|
|
23
|
+
inputTokens: number | null;
|
|
24
|
+
outputTokens: number | null;
|
|
25
|
+
cachedInputTokens: number | null;
|
|
26
|
+
reasoningTokens: number | null;
|
|
27
|
+
totalTokens: number | null;
|
|
28
|
+
}
|
|
29
|
+
export interface RequestTrace {
|
|
30
|
+
request: number;
|
|
31
|
+
purpose: "agent" | "compaction";
|
|
32
|
+
startedAtMs: number;
|
|
33
|
+
endedAtMs: number | null;
|
|
34
|
+
firstContentAtMs: number | null;
|
|
35
|
+
status: number | null;
|
|
36
|
+
requestId: string | null;
|
|
37
|
+
servedModel: string | null;
|
|
38
|
+
systemFingerprint: string | null;
|
|
39
|
+
finishReason: string | null;
|
|
40
|
+
usage: ReportedUsage | null;
|
|
41
|
+
error: string | null;
|
|
42
|
+
streamComplete: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ToolTrace {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
startedAtMs: number | null;
|
|
48
|
+
endedAtMs: number | null;
|
|
49
|
+
isError: boolean | null;
|
|
50
|
+
error: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface Observation {
|
|
53
|
+
requests: RequestTrace[];
|
|
54
|
+
tools: ToolTrace[];
|
|
55
|
+
agentTurns: number;
|
|
56
|
+
assistantMessages: number;
|
|
57
|
+
toolCalls: number;
|
|
58
|
+
toolCallsByType: Record<string, number>;
|
|
59
|
+
compactions: number;
|
|
60
|
+
retries: number;
|
|
61
|
+
agentStartedAtMs: number | null;
|
|
62
|
+
agentEndedAtMs: number | null;
|
|
63
|
+
systemPromptHash: string | null;
|
|
64
|
+
lastAssistantStopReason: string | null;
|
|
65
|
+
errors: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface CommandResult {
|
|
68
|
+
command: Command;
|
|
69
|
+
exitCode: number | null;
|
|
70
|
+
signal: string | null;
|
|
71
|
+
durationMs: number;
|
|
72
|
+
timedOut: boolean;
|
|
73
|
+
output: string;
|
|
74
|
+
outputTruncated: boolean;
|
|
75
|
+
}
|
|
76
|
+
export type Failure =
|
|
77
|
+
| "validation_failed"
|
|
78
|
+
| "timeout"
|
|
79
|
+
| "model_api_error"
|
|
80
|
+
| "rate_limit"
|
|
81
|
+
| "tool_error"
|
|
82
|
+
| "agent_error"
|
|
83
|
+
| "context_limit"
|
|
84
|
+
| "cancelled"
|
|
85
|
+
| "unknown";
|
|
86
|
+
|
|
87
|
+
export interface WorkerInput {
|
|
88
|
+
modelSettings?: ModelSettings;
|
|
89
|
+
definition: BenchmarkDefinition;
|
|
90
|
+
model: NebiusModel;
|
|
91
|
+
agentDir: string;
|
|
92
|
+
apiKey: string;
|
|
93
|
+
}
|
|
94
|
+
export type WorkerMessage =
|
|
95
|
+
| { type: "settings"; effectiveSettings: Record<string, unknown> }
|
|
96
|
+
| { type: "observation"; observation: Observation }
|
|
97
|
+
| { type: "done"; error: string | null; effectiveSettings: Record<string, unknown> };
|
|
98
|
+
|
|
99
|
+
export interface TokenTotals {
|
|
100
|
+
cumulativeInputTokens: number | null;
|
|
101
|
+
cumulativeOutputTokens: number | null;
|
|
102
|
+
cachedInputTokens: number | null;
|
|
103
|
+
reasoningTokens: number | null;
|
|
104
|
+
observedInputTokens: number;
|
|
105
|
+
observedOutputTokens: number;
|
|
106
|
+
requestsWithUsage: number;
|
|
107
|
+
usageComplete: boolean;
|
|
108
|
+
lastRequestInputTokens: number | null;
|
|
109
|
+
finalContextSizeTokens: null;
|
|
110
|
+
inputAmplification: null;
|
|
111
|
+
inputAmplificationVsLastRequest: number | null;
|
|
112
|
+
}
|
|
113
|
+
export interface RunResult {
|
|
114
|
+
schemaVersion: 2;
|
|
115
|
+
id: string;
|
|
116
|
+
benchmark: string;
|
|
117
|
+
model: string;
|
|
118
|
+
modelRevision: null;
|
|
119
|
+
run: number;
|
|
120
|
+
timestamp: string;
|
|
121
|
+
success: boolean;
|
|
122
|
+
failure: Failure | null;
|
|
123
|
+
errors: string[];
|
|
124
|
+
wallTimeMs: number;
|
|
125
|
+
agentWallTimeMs: number;
|
|
126
|
+
modelRequestWallTimeMs: number | null;
|
|
127
|
+
toolExecutionTimeMs: number | null;
|
|
128
|
+
timeToFirstContentMs: number | null;
|
|
129
|
+
modelGenerationTimeMs: null;
|
|
130
|
+
agentOverheadTimeMs: null;
|
|
131
|
+
agentTurns: number;
|
|
132
|
+
modelRequests: number;
|
|
133
|
+
toolCalls: number;
|
|
134
|
+
toolErrors: number;
|
|
135
|
+
toolCallsByType: Record<string, number>;
|
|
136
|
+
tokens: TokenTotals;
|
|
137
|
+
validation: {
|
|
138
|
+
checked?: boolean;
|
|
139
|
+
passed: boolean;
|
|
140
|
+
exitCode: number | null;
|
|
141
|
+
durationMs: number;
|
|
142
|
+
commands: CommandResult[];
|
|
143
|
+
};
|
|
144
|
+
setup: CommandResult[];
|
|
145
|
+
observation: Observation;
|
|
146
|
+
workspace: string;
|
|
147
|
+
fixtureHash: string;
|
|
148
|
+
finalWorkspaceHash: string | null;
|
|
149
|
+
effectiveSettings: Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
export interface Distribution {
|
|
152
|
+
count: number;
|
|
153
|
+
missing: number;
|
|
154
|
+
mean: number | null;
|
|
155
|
+
median: number | null;
|
|
156
|
+
min: number | null;
|
|
157
|
+
max: number | null;
|
|
158
|
+
standardDeviation: number | null;
|
|
159
|
+
}
|
|
160
|
+
export interface ModelAggregate {
|
|
161
|
+
model: string;
|
|
162
|
+
runs: number;
|
|
163
|
+
successes: number;
|
|
164
|
+
successRate: number | null;
|
|
165
|
+
wallTimeMs: Distribution;
|
|
166
|
+
inputTokens: Distribution;
|
|
167
|
+
outputTokens: Distribution;
|
|
168
|
+
turns: Distribution;
|
|
169
|
+
toolCalls: Distribution;
|
|
170
|
+
}
|
|
171
|
+
export interface Results {
|
|
172
|
+
schemaVersion: 2;
|
|
173
|
+
status: "running" | "complete" | "cancelled";
|
|
174
|
+
timestamp: string;
|
|
175
|
+
metadata: Record<string, unknown>;
|
|
176
|
+
definition: BenchmarkDefinition;
|
|
177
|
+
plannedRuns: number;
|
|
178
|
+
runs: RunResult[];
|
|
179
|
+
aggregates: ModelAggregate[];
|
|
180
|
+
}
|