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,101 @@
1
+ import type {
2
+ Distribution,
3
+ ModelAggregate,
4
+ RequestTrace,
5
+ RunResult,
6
+ TokenTotals,
7
+ } from "./types.ts";
8
+
9
+ export function tokenTotals(requests: RequestTrace[]): TokenTotals {
10
+ const sum = (field: "inputTokens" | "outputTokens" | "cachedInputTokens" | "reasoningTokens") =>
11
+ requests.reduce((total, request) => total + (request.usage?.[field] ?? 0), 0);
12
+ const complete = (
13
+ field: "inputTokens" | "outputTokens" | "cachedInputTokens" | "reasoningTokens",
14
+ ) => requests.length > 0 && requests.every((request) => request.usage?.[field] != null);
15
+ const input = complete("inputTokens") ? sum("inputTokens") : null;
16
+ const last =
17
+ requests.filter((request) => request.purpose === "agent").at(-1)?.usage?.inputTokens ?? null;
18
+ return {
19
+ cumulativeInputTokens: input,
20
+ cumulativeOutputTokens: complete("outputTokens") ? sum("outputTokens") : null,
21
+ cachedInputTokens: complete("cachedInputTokens") ? sum("cachedInputTokens") : null,
22
+ reasoningTokens: complete("reasoningTokens") ? sum("reasoningTokens") : null,
23
+ observedInputTokens: sum("inputTokens"),
24
+ observedOutputTokens: sum("outputTokens"),
25
+ requestsWithUsage: requests.filter(
26
+ (request) => request.usage?.inputTokens != null && request.usage.outputTokens != null,
27
+ ).length,
28
+ usageComplete: complete("inputTokens") && complete("outputTokens"),
29
+ lastRequestInputTokens: last,
30
+ finalContextSizeTokens: null,
31
+ inputAmplification: null,
32
+ inputAmplificationVsLastRequest:
33
+ input !== null && last !== null && last > 0 ? input / last : null,
34
+ };
35
+ }
36
+
37
+ export function distribution(values: Array<number | null>): Distribution {
38
+ const known = values
39
+ .filter((value): value is number => value !== null && Number.isFinite(value))
40
+ .sort((a, b) => a - b);
41
+ const count = known.length;
42
+ if (!count)
43
+ return {
44
+ count: 0,
45
+ missing: values.length,
46
+ mean: null,
47
+ median: null,
48
+ min: null,
49
+ max: null,
50
+ standardDeviation: null,
51
+ };
52
+ const mean = known.reduce((a, b) => a + b, 0) / count;
53
+ return {
54
+ count,
55
+ missing: values.length - count,
56
+ mean,
57
+ median: ((known[Math.floor((count - 1) / 2)] ?? 0) + (known[Math.floor(count / 2)] ?? 0)) / 2,
58
+ min: known[0] ?? null,
59
+ max: known.at(-1) ?? null,
60
+ standardDeviation: Math.sqrt(
61
+ known.reduce((total, value) => total + (value - mean) ** 2, 0) / count,
62
+ ),
63
+ };
64
+ }
65
+ export function aggregate(runs: RunResult[]): ModelAggregate[] {
66
+ return [...new Set(runs.map((run) => run.model))].map((model) => {
67
+ const group = runs.filter((run) => run.model === model);
68
+ const successes = group.filter((run) => run.success).length;
69
+ return {
70
+ model,
71
+ runs: group.length,
72
+ successes,
73
+ successRate: group.every((run) => run.validation?.checked === false)
74
+ ? null
75
+ : successes / group.length,
76
+ wallTimeMs: distribution(group.map((run) => run.wallTimeMs)),
77
+ inputTokens: distribution(group.map((run) => run.tokens.cumulativeInputTokens)),
78
+ outputTokens: distribution(group.map((run) => run.tokens.cumulativeOutputTokens)),
79
+ turns: distribution(group.map((run) => run.agentTurns)),
80
+ toolCalls: distribution(group.map((run) => run.toolCalls)),
81
+ };
82
+ });
83
+ }
84
+ /** Union, rather than sum, avoids double-counting parallel tool/request intervals. */
85
+ export function intervalDuration(
86
+ intervals: Array<{ startedAtMs: number | null; endedAtMs: number | null }>,
87
+ ): number {
88
+ const ordered = intervals
89
+ .filter(
90
+ (item): item is { startedAtMs: number; endedAtMs: number } =>
91
+ item.startedAtMs !== null && item.endedAtMs !== null,
92
+ )
93
+ .sort((a, b) => a.startedAtMs - b.startedAtMs);
94
+ let total = 0;
95
+ let end = 0;
96
+ for (const item of ordered) {
97
+ total += Math.max(0, item.endedAtMs - Math.max(end, item.startedAtMs));
98
+ end = Math.max(end, item.endedAtMs);
99
+ }
100
+ return total;
101
+ }
@@ -0,0 +1,120 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import type { Command, CommandResult } from "./types.ts";
4
+
5
+ export function cleanEnvironment(): NodeJS.ProcessEnv {
6
+ return { PATH: process.env.PATH, LANG: "C.UTF-8", LC_ALL: "C.UTF-8", TZ: "UTC" };
7
+ }
8
+ export function terminateGroup(pid: number, signal: NodeJS.Signals = "SIGTERM") {
9
+ try {
10
+ process.kill(-pid, signal);
11
+ } catch {
12
+ try {
13
+ process.kill(pid, signal);
14
+ } catch {
15
+ /* Already exited. */
16
+ }
17
+ }
18
+ }
19
+ export async function descendants(pid: number): Promise<number[]> {
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
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+ export async function runCommand(
44
+ command: Command,
45
+ cwd: string,
46
+ timeoutMs: number,
47
+ signal?: AbortSignal,
48
+ redact: (text: string) => string = (text) => text,
49
+ ): Promise<CommandResult> {
50
+ const started = performance.now();
51
+ if (signal?.aborted)
52
+ return {
53
+ command,
54
+ exitCode: null,
55
+ signal: "SIGINT",
56
+ durationMs: 0,
57
+ timedOut: false,
58
+ output: "Cancelled before validation",
59
+ outputTruncated: false,
60
+ };
61
+ return new Promise((resolve) => {
62
+ const child = spawn(command.command, command.args, {
63
+ cwd,
64
+ env: cleanEnvironment(),
65
+ detached: true,
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ });
68
+ const chunks: Buffer[] = [];
69
+ let bytes = 0;
70
+ let truncated = false;
71
+ let timedOut = false;
72
+ let settled = false;
73
+ let force: NodeJS.Timeout | undefined;
74
+ const stop = () => {
75
+ if (!child.pid) return;
76
+ terminateGroup(child.pid);
77
+ force ??= setTimeout(() => {
78
+ if (child.pid) terminateGroup(child.pid, "SIGKILL");
79
+ }, 1000);
80
+ };
81
+ const timer = setTimeout(() => {
82
+ timedOut = true;
83
+ stop();
84
+ }, timeoutMs);
85
+ signal?.addEventListener("abort", stop, { once: true });
86
+ const capture = (data: Buffer) => {
87
+ const remaining = 256 * 1024 - bytes;
88
+ if (data.length > remaining) truncated = true;
89
+ if (remaining > 0) {
90
+ chunks.push(data.subarray(0, remaining));
91
+ bytes += Math.min(remaining, data.length);
92
+ }
93
+ };
94
+ child.stdout.on("data", capture);
95
+ child.stderr.on("data", capture);
96
+ const finish = (code: number | null, exitSignal: string | null, error?: string) => {
97
+ if (settled) return;
98
+ settled = true;
99
+ clearTimeout(timer);
100
+ if (force) clearTimeout(force);
101
+ signal?.removeEventListener("abort", stop);
102
+ // Remove descendants that kept stdio open after their command exited.
103
+ if (child.pid) terminateGroup(child.pid, "SIGKILL");
104
+ resolve({
105
+ command,
106
+ exitCode: code,
107
+ signal: exitSignal,
108
+ durationMs: performance.now() - started,
109
+ timedOut,
110
+ output: redact(error ?? Buffer.concat(chunks).toString("utf8")),
111
+ outputTruncated: truncated,
112
+ });
113
+ };
114
+ child.on("error", (error) => finish(null, null, String(error)));
115
+ child.on("exit", (code, exitSignal) => {
116
+ if (child.pid) terminateGroup(child.pid, "SIGKILL");
117
+ child.once("close", () => finish(code, exitSignal));
118
+ });
119
+ });
120
+ }
@@ -0,0 +1,71 @@
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
+
5
+ const excluded = (path: string) =>
6
+ path
7
+ .split(/[\\/]/)
8
+ .some(
9
+ (name) =>
10
+ [
11
+ ".git",
12
+ ".pi",
13
+ "node_modules",
14
+ "benchmark-results",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ ".next",
19
+ ].includes(name) ||
20
+ name === ".env" ||
21
+ name.startsWith(".env.") ||
22
+ /\.(pem|key)$/.test(name),
23
+ );
24
+
25
+ /** Snapshot working files, respecting Git ignores, without dependency trees or known credential files. */
26
+ export async function snapshotProject(source: string, target: string) {
27
+ let files: string[];
28
+ try {
29
+ files = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
30
+ cwd: source,
31
+ encoding: "utf8",
32
+ stdio: ["ignore", "pipe", "ignore"],
33
+ maxBuffer: 8 * 1024 * 1024,
34
+ })
35
+ .split("\0")
36
+ .filter(Boolean);
37
+ } catch {
38
+ const walk = async (prefix = ""): Promise<string[]> => {
39
+ const found: string[] = [];
40
+ for (const entry of await readdir(join(source, prefix), { withFileTypes: true })) {
41
+ const path = join(prefix, entry.name);
42
+ if (excluded(path)) continue;
43
+ if (entry.isDirectory()) found.push(...(await walk(path)));
44
+ else found.push(path);
45
+ }
46
+ return found;
47
+ };
48
+ files = await walk();
49
+ }
50
+ await mkdir(target, { recursive: true, mode: 0o700 });
51
+ let bytes = 0;
52
+ let copied = 0;
53
+ for (const path of new Set(files)) {
54
+ if (excluded(path)) continue;
55
+ const info = await lstat(join(source, path)).catch((error) => {
56
+ if (error.code === "ENOENT") return undefined;
57
+ throw error;
58
+ });
59
+ if (!info) continue;
60
+ if (!info.isFile())
61
+ throw new Error(`Cannot snapshot ${path}: only regular project files are supported.`);
62
+ bytes += info.size;
63
+ if (++copied > 10000 || bytes > 50 * 1024 * 1024)
64
+ throw new Error(
65
+ "Project snapshot exceeds 10,000 files or 50 MiB. Run Pi from a smaller project directory.",
66
+ );
67
+ await mkdir(dirname(join(target, path)), { recursive: true, mode: 0o700 });
68
+ await cp(join(source, path), join(target, path));
69
+ }
70
+ return copied;
71
+ }
@@ -0,0 +1,111 @@
1
+ import { distribution } from "./metrics.ts";
2
+ import type { Results, RunResult } from "./types.ts";
3
+
4
+ /** Client-observed first generated content on the first agent request, not worker startup. */
5
+ export function observedTtftMs(run: RunResult): number | null {
6
+ const first = run.observation.requests.find((request) => request.purpose === "agent");
7
+ if (first?.firstContentAtMs == null) return null;
8
+ const elapsed = first.firstContentAtMs - first.startedAtMs;
9
+ return elapsed >= 0 ? elapsed : null;
10
+ }
11
+
12
+ /** End-to-end output rate, including tools, network, setup, and validation. */
13
+ export function outputThroughput(runs: RunResult[]): number | null {
14
+ if (
15
+ !runs.length ||
16
+ runs.some((run) => run.tokens.cumulativeOutputTokens === null || run.wallTimeMs <= 0)
17
+ )
18
+ return null;
19
+ const tokens = runs.reduce((sum, run) => sum + (run.tokens.cumulativeOutputTokens ?? 0), 0);
20
+ return tokens / (runs.reduce((sum, run) => sum + run.wallTimeMs, 0) / 1000);
21
+ }
22
+
23
+ function failureDetails(run: RunResult): string {
24
+ const failed = run.validation.commands.find(
25
+ (command) => command.exitCode !== 0 || command.timedOut,
26
+ );
27
+ const output = failed?.output ?? "";
28
+ const reason =
29
+ output.match(/\berror: \|-\n\s+([^\n]+)/)?.[1] ?? output.match(/\berror: (.+)/)?.[1];
30
+ const named = output.match(/not ok \d+ - (.+)/)?.[1];
31
+ const location = output.match(/TestContext[^\n]*\/([^/\n]+\.(?:mjs|js|ts):\d+:\d+)/)?.[1];
32
+ const detail = reason && reason !== "|-" ? reason : named;
33
+ return [run.errors[0] || detail, location]
34
+ .filter(Boolean)
35
+ .join("; ")
36
+ .replace(/\s+/g, " ")
37
+ .slice(0, 350);
38
+ }
39
+
40
+ export function terminalReport(results: Results): string {
41
+ const unchecked = results.definition.validationMode === "none";
42
+ const groups = results.aggregates;
43
+ const runs = groups.map((group) => results.runs.filter((run) => run.model === group.model));
44
+ const number = (value: number | null, decimals = 1) =>
45
+ value === null
46
+ ? "n/a"
47
+ : value.toLocaleString("en-US", {
48
+ minimumFractionDigits: decimals,
49
+ maximumFractionDigits: decimals,
50
+ });
51
+ const seconds = (value: number | null) =>
52
+ value === null ? "n/a" : `${number(value / 1000, 2)} s`;
53
+ const rows = [
54
+ ["Metric", ...groups.map((group) => group.model)],
55
+ [
56
+ unchecked ? "Finished runs (not validated)" : "Validated success",
57
+ ...groups.map(
58
+ (group, index) =>
59
+ `${unchecked ? runs[index]?.filter((run) => run.failure === null).length : group.successes}/${group.runs}`,
60
+ ),
61
+ ],
62
+ ["Input tokens / run (mean)", ...groups.map((group) => number(group.inputTokens.mean, 0))],
63
+ ["Output tokens / run (mean)", ...groups.map((group) => number(group.outputTokens.mean, 0))],
64
+ ["Task duration (median)", ...groups.map((group) => seconds(group.wallTimeMs.median))],
65
+ [
66
+ "Observed TTFT (median)",
67
+ ...runs.map((group) => seconds(distribution(group.map(observedTtftMs)).median)),
68
+ ],
69
+ ["Throughput (output tokens/s)", ...runs.map((group) => number(outputThroughput(group)))],
70
+ ["Agent turns / run (mean)", ...groups.map((group) => number(group.turns.mean))],
71
+ ["Tool calls / run (mean)", ...groups.map((group) => number(group.toolCalls.mean))],
72
+ ];
73
+ const widths =
74
+ rows[0]?.map((_, index) => Math.max(...rows.map((row) => row[index]?.length ?? 0))) ?? [];
75
+ const separator = widths.map((width) => "─".repeat(width)).join("─┼─");
76
+ const table = rows.map((row) =>
77
+ row
78
+ .map((cell, index) => cell.padEnd(widths[index] ?? 0))
79
+ .join(" │ ")
80
+ .trimEnd(),
81
+ );
82
+ table.splice(1, 0, separator);
83
+ const failures = results.runs.filter((run) => run.failure !== null);
84
+ return [
85
+ `Benchmark: ${results.definition.name}`,
86
+ `Completed: ${results.runs.length}/${results.plannedRuns} (${results.status})`,
87
+ ...(unchecked
88
+ ? ["Correctness: not checked. Finished means execution completed without an error."]
89
+ : []),
90
+ "",
91
+ ...table,
92
+ "",
93
+ "Tokens include every request in a run. Failed runs contribute to these metrics too.",
94
+ "TTFT: first agent request start → first streamed text/reasoning/tool content; excludes worker startup.",
95
+ "Stream chunks may contain multiple tokens; TTFT is client-observed, not server token timing.",
96
+ "Throughput: total output tokens ÷ total task seconds, including network, tools, and validation.",
97
+ "Unknown values are n/a. Means/medians exclude missing values; throughput needs complete output usage for every run.",
98
+ ...(failures.length
99
+ ? [
100
+ "",
101
+ "Failed runs:",
102
+ ...failures.map((run) => {
103
+ const details = failureDetails(run);
104
+ return `- ${run.model} #${run.run}: ${run.failure}${details ? ` — ${details}` : ""}`;
105
+ }),
106
+ ]
107
+ : []),
108
+ "",
109
+ "Detailed per-run measurements and validator output: results.json.",
110
+ ].join("\n");
111
+ }