offgrid-ai 0.13.0 → 0.14.1
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/package.json +4 -1
- package/src/benchmark/flow.mjs +32 -47
- package/src/benchmark/prepare.mjs +2 -5
- package/src/benchmark/sdk-runner.mjs +363 -0
- package/src/benchmark/shared.mjs +0 -8
- package/src/benchmark.mjs +2 -2
- package/src/commands/run.mjs +0 -12
- package/src/harness-pi.mjs +3 -3
- package/src/model-presenters.mjs +0 -15
- package/src/process.mjs +5 -1
- package/src/profile-setup.mjs +8 -73
- package/src/profiles.mjs +4 -21
- package/src/benchmark/pi-runner.mjs +0 -257
- package/src/benchmark/stream-renderer.mjs +0 -302
- package/src/command.mjs +0 -21
package/src/profile-setup.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { detectCapabilities } from "./autodetect.mjs";
|
|
|
9
9
|
import { matchDrafter } from "./scan.mjs";
|
|
10
10
|
import { scanGgufModels } from "./scan.mjs";
|
|
11
11
|
import { estimateMemoryMb } from "./mlx-flags.mjs";
|
|
12
|
+
import { capabilitySummary } from "./model-summary.mjs";
|
|
12
13
|
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
|
|
@@ -70,7 +71,7 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
70
71
|
console.log("");
|
|
71
72
|
console.log(renderSection("Model setup", renderRows([
|
|
72
73
|
["Model", pc.bold(profile.label)],
|
|
73
|
-
["Detected",
|
|
74
|
+
["Detected", capabilitySummary(caps)],
|
|
74
75
|
["Context", `${profile.flags.ctxSize.toLocaleString()} tokens`],
|
|
75
76
|
["KV cache", `${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV}`],
|
|
76
77
|
["Sampling", samplingSummary(profile.flags)],
|
|
@@ -151,16 +152,12 @@ export function applyRuntimeFlagOverrides(profile, overrides) {
|
|
|
151
152
|
|
|
152
153
|
export function applyMtpDefaults(profile) {
|
|
153
154
|
const flags = { ...profile.flags, port: LLAMA_CPP_MTP_PORT };
|
|
154
|
-
const edits = {
|
|
155
|
-
values: { "--spec-type": "draft-mtp", "--spec-draft-n-max": 4 },
|
|
156
|
-
};
|
|
157
|
-
if (profile.drafterPath) edits.values["--spec-draft-model"] = profile.drafterPath;
|
|
158
155
|
return applyProfileFlags({
|
|
159
156
|
...profile,
|
|
160
157
|
backend: "llama-cpp-mtp",
|
|
161
158
|
providerId: "llama-cpp-mtp",
|
|
162
159
|
capabilities: { ...(profile.capabilities ?? {}), mtp: true },
|
|
163
|
-
}, flags
|
|
160
|
+
}, flags);
|
|
164
161
|
}
|
|
165
162
|
|
|
166
163
|
export function removeMtpDefaults(profile) {
|
|
@@ -171,9 +168,7 @@ export function removeMtpDefaults(profile) {
|
|
|
171
168
|
providerId: "llama-cpp",
|
|
172
169
|
drafterPath: null,
|
|
173
170
|
capabilities: { ...(profile.capabilities ?? {}), mtp: false },
|
|
174
|
-
}, flags
|
|
175
|
-
remove: ["--spec-type", "--spec-draft-n-max", "--spec-draft-model"],
|
|
176
|
-
});
|
|
171
|
+
}, flags);
|
|
177
172
|
}
|
|
178
173
|
|
|
179
174
|
function applyVisionDefaults(profile) {
|
|
@@ -201,10 +196,10 @@ function applyThinkingDefaults(profile) {
|
|
|
201
196
|
function removeThinkingDefaults(profile) {
|
|
202
197
|
const flags = { ...profile.flags, ...GENERAL_DEFAULTS };
|
|
203
198
|
delete flags.chatTemplateKwargs;
|
|
204
|
-
return applyProfileFlags(profile, flags
|
|
199
|
+
return applyProfileFlags(profile, flags);
|
|
205
200
|
}
|
|
206
201
|
|
|
207
|
-
function applyProfileFlags(profile, flags
|
|
202
|
+
function applyProfileFlags(profile, flags) {
|
|
208
203
|
const next = {
|
|
209
204
|
...profile,
|
|
210
205
|
flags,
|
|
@@ -214,43 +209,9 @@ function applyProfileFlags(profile, flags, edits = {}) {
|
|
|
214
209
|
pi: { ...(profile.harnesses?.pi ?? {}), enabled: true, model: `${profile.providerId ?? profile.backend}/${profile.modelAlias ?? profile.id}` },
|
|
215
210
|
},
|
|
216
211
|
};
|
|
217
|
-
next.commandArgv = updateArgv(profile.commandArgv ?? [], {
|
|
218
|
-
"--host": flags.host,
|
|
219
|
-
"--port": flags.port,
|
|
220
|
-
"--ctx-size": flags.ctxSize,
|
|
221
|
-
"--cache-type-k": flags.cacheTypeK,
|
|
222
|
-
"--cache-type-v": flags.cacheTypeV,
|
|
223
|
-
"--top-k": flags.topK,
|
|
224
|
-
"--presence-penalty": flags.presencePenalty,
|
|
225
|
-
"--repeat-penalty": flags.repeatPenalty,
|
|
226
|
-
...(flags.chatTemplateKwargs ? { "--chat-template-kwargs": JSON.stringify(flags.chatTemplateKwargs) } : {}),
|
|
227
|
-
}, edits);
|
|
228
|
-
return next;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function updateArgv(argv, values, edits = {}) {
|
|
232
|
-
let next = [...argv];
|
|
233
|
-
for (const flag of edits.remove ?? []) next = removeOption(next, flag);
|
|
234
|
-
for (const [flag, value] of Object.entries({ ...values, ...(edits.values ?? {}) })) {
|
|
235
|
-
if (value === undefined) continue;
|
|
236
|
-
const index = next.indexOf(flag);
|
|
237
|
-
if (index === -1) next.push(flag, String(value));
|
|
238
|
-
else next[index + 1] = String(value);
|
|
239
|
-
}
|
|
240
212
|
return next;
|
|
241
213
|
}
|
|
242
214
|
|
|
243
|
-
function removeOption(argv, flag) {
|
|
244
|
-
const next = [];
|
|
245
|
-
for (let i = 0; i < argv.length; i++) {
|
|
246
|
-
if (argv[i] === flag) {
|
|
247
|
-
if (argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
next.push(argv[i]);
|
|
251
|
-
}
|
|
252
|
-
return next;
|
|
253
|
-
}
|
|
254
215
|
|
|
255
216
|
function renderMemoryEstimate(profile) {
|
|
256
217
|
try {
|
|
@@ -283,18 +244,6 @@ async function runtimeSupportsGemma4Unified() {
|
|
|
283
244
|
}
|
|
284
245
|
}
|
|
285
246
|
|
|
286
|
-
function detectionSummary(caps) {
|
|
287
|
-
const parts = [];
|
|
288
|
-
if (caps.architecture) parts.push(caps.architecture);
|
|
289
|
-
if (caps.quant) parts.push(caps.quant);
|
|
290
|
-
if (caps.mtp) parts.push("MTP");
|
|
291
|
-
if (caps.qat) parts.push("QAT");
|
|
292
|
-
|
|
293
|
-
if (caps.thinking) parts.push("thinking");
|
|
294
|
-
if (caps.vision) parts.push("vision");
|
|
295
|
-
return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
|
|
296
|
-
}
|
|
297
|
-
|
|
298
247
|
function samplingSummary(flags) {
|
|
299
248
|
return `temp ${flags.temperature}, top-p ${flags.topP}, top-k ${flags.topK}`;
|
|
300
249
|
}
|
|
@@ -342,7 +291,7 @@ export async function configureMlxProfile(prompt, profile) {
|
|
|
342
291
|
["Backend", configured.backend],
|
|
343
292
|
["Endpoint", configured.baseUrl],
|
|
344
293
|
["Context", String(configured.flags.ctxSize) + " tokens"],
|
|
345
|
-
["Thinking", configured.capabilities
|
|
294
|
+
["Thinking", configured.capabilities?.thinking ? "on" : "off"],
|
|
346
295
|
["Vision", configured.capabilities.vision ? "yes" : "no"],
|
|
347
296
|
])));
|
|
348
297
|
|
|
@@ -352,33 +301,19 @@ export async function configureMlxProfile(prompt, profile) {
|
|
|
352
301
|
|
|
353
302
|
async function applyMlxThinkingToggle(profile, enabled) {
|
|
354
303
|
if (!profile.capabilities.thinking) return profile;
|
|
355
|
-
const { computeMlxVlmFlags } = await import("./mlx-flags.mjs");
|
|
356
|
-
const { args } = computeMlxVlmFlags(profile.modelPath, {
|
|
357
|
-
port: profile.flags.port,
|
|
358
|
-
ctxSize: profile.flags.ctxSize,
|
|
359
|
-
thinkingEnabled: enabled,
|
|
360
|
-
});
|
|
361
304
|
return {
|
|
362
305
|
...profile,
|
|
363
|
-
commandArgv: args,
|
|
364
306
|
capabilities: { ...profile.capabilities, thinkingEnabled: enabled },
|
|
365
307
|
};
|
|
366
308
|
}
|
|
367
309
|
|
|
368
310
|
function applyMlxContextSize(profile, ctxSize) {
|
|
369
311
|
const flags = { ...profile.flags, ctxSize };
|
|
370
|
-
|
|
312
|
+
return {
|
|
371
313
|
...profile,
|
|
372
314
|
flags,
|
|
373
315
|
baseUrl: baseUrlForFlags(flags),
|
|
374
316
|
};
|
|
375
|
-
const idx = next.commandArgv.indexOf("--max-kv-size");
|
|
376
|
-
if (idx !== -1 && next.commandArgv[idx + 1] != null) {
|
|
377
|
-
next.commandArgv[idx + 1] = String(ctxSize);
|
|
378
|
-
} else if (ctxSize && ctxSize > 0) {
|
|
379
|
-
next.commandArgv.push("--max-kv-size", String(ctxSize));
|
|
380
|
-
}
|
|
381
|
-
return next;
|
|
382
317
|
}
|
|
383
318
|
|
|
384
319
|
function renderMlxMemoryEstimate(profile) {
|
package/src/profiles.mjs
CHANGED
|
@@ -18,10 +18,6 @@ export function profileJsonPath(id) {
|
|
|
18
18
|
return join(profileDir(id), "profile.json");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function commandJsonPath(id) {
|
|
22
|
-
return join(profileDir(id), "command.json");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
export function notesPath(id) {
|
|
26
22
|
return join(profileDir(id), "notes.md");
|
|
27
23
|
}
|
|
@@ -69,9 +65,8 @@ export async function saveProfile(profile, options = {}) {
|
|
|
69
65
|
};
|
|
70
66
|
await writeJson(profileJsonPath(id), saved);
|
|
71
67
|
|
|
72
|
-
// Note:
|
|
73
|
-
// fresh from
|
|
74
|
-
// process.mjs). commandArgv is kept in the profile for backwards compat.
|
|
68
|
+
// Note: commandArgv is no longer stored — computeServerCommand computes
|
|
69
|
+
// the full command fresh from profile config at launch time.
|
|
75
70
|
|
|
76
71
|
if (!existsSync(notesPath(id))) {
|
|
77
72
|
await writeFile(notesPath(id), `# ${saved.label}\n\nNotes for this model profile.\n`, "utf8");
|
|
@@ -134,7 +129,7 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
|
134
129
|
// If a drafter is provided, this model supports MTP regardless of filename
|
|
135
130
|
const hasMtp = caps.mtp || Boolean(drafterPath);
|
|
136
131
|
const backend = backendId ?? (hasMtp ? "llama-cpp-mtp" : "llama-cpp");
|
|
137
|
-
const { flags
|
|
132
|
+
const { flags } = computeFlags(
|
|
138
133
|
{ ...caps, mtp: hasMtp },
|
|
139
134
|
model.path,
|
|
140
135
|
model.mmprojPath,
|
|
@@ -154,21 +149,15 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
|
154
149
|
capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp }),
|
|
155
150
|
preset: null, // no presets — auto-detected
|
|
156
151
|
flags,
|
|
157
|
-
commandArgv: argv,
|
|
158
152
|
});
|
|
159
153
|
}
|
|
160
154
|
|
|
161
155
|
// ── Auto-create profile from a discovered MLX model ────────────────────────
|
|
162
156
|
|
|
163
157
|
export async function createProfileFromMlxModel(model) {
|
|
164
|
-
const {
|
|
158
|
+
const { DEFAULT_PORT } = await import("./mlx-flags.mjs");
|
|
165
159
|
const caps = await detectMlxCapabilities(model.filePath);
|
|
166
160
|
const ctxSize = defaultMlxContextLength(caps.contextLength, detectHardware().totalRamBytes / (1024 ** 3));
|
|
167
|
-
const { args } = computeMlxVlmFlags(model.filePath, {
|
|
168
|
-
port: DEFAULT_PORT,
|
|
169
|
-
ctxSize,
|
|
170
|
-
thinkingEnabled: caps.thinking,
|
|
171
|
-
});
|
|
172
161
|
return normalizeProfile({
|
|
173
162
|
id: slugFromLabel(model.label),
|
|
174
163
|
label: model.label,
|
|
@@ -182,7 +171,6 @@ export async function createProfileFromMlxModel(model) {
|
|
|
182
171
|
modelSizeBytes: model.sizeBytes,
|
|
183
172
|
capabilities: caps,
|
|
184
173
|
flags: { host: "127.0.0.1", port: DEFAULT_PORT, ctxSize },
|
|
185
|
-
commandArgv: args,
|
|
186
174
|
});
|
|
187
175
|
}
|
|
188
176
|
|
|
@@ -203,11 +191,6 @@ function summarizeCapabilities(caps) {
|
|
|
203
191
|
|
|
204
192
|
// ── State files (for running servers) ──────────────────────────────────────
|
|
205
193
|
|
|
206
|
-
export async function readCommandArgv(profile) {
|
|
207
|
-
const command = await readJson(commandJsonPath(profile.id), null);
|
|
208
|
-
return Array.isArray(command?.argv) ? command.argv.map(String) : (profile.commandArgv ?? []).map(String);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
194
|
export async function readState(id) {
|
|
212
195
|
return readJson(statePath(id), null);
|
|
213
196
|
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
// ── Run benchmark in Pi (non-interactive JSON mode) ───────────────────────────
|
|
2
|
-
|
|
3
|
-
import { writeFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
7
|
-
BENCH_COLORS, renderStreamEvent,
|
|
8
|
-
formatToolCall, printFinalLine, stopExecTimer,
|
|
9
|
-
} from "./stream-renderer.mjs";
|
|
10
|
-
import { piModelString } from "./shared.mjs";
|
|
11
|
-
|
|
12
|
-
export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
|
|
13
|
-
const model = piModelString(profile);
|
|
14
|
-
const args = ["--model", model, "--mode", "json", "-p", "@prompt.md"];
|
|
15
|
-
|
|
16
|
-
const child = spawn("pi", args, {
|
|
17
|
-
cwd: runDirectory,
|
|
18
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const runResult = {
|
|
22
|
-
model,
|
|
23
|
-
exitCode: null,
|
|
24
|
-
wallClockMs: null,
|
|
25
|
-
agentTurns: 0,
|
|
26
|
-
promptTokens: 0,
|
|
27
|
-
completionTokens: 0,
|
|
28
|
-
totalTokens: 0,
|
|
29
|
-
cacheRead: 0,
|
|
30
|
-
cacheWrite: 0,
|
|
31
|
-
toolCalls: 0,
|
|
32
|
-
toolResults: 0,
|
|
33
|
-
perTurn: [],
|
|
34
|
-
rawResponseLines: [],
|
|
35
|
-
error: null,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
let streamBuffer = "";
|
|
39
|
-
let responseBuffer = "";
|
|
40
|
-
let currentTurnStartMs = null;
|
|
41
|
-
let lastTurnEndMs = null;
|
|
42
|
-
let runStartMs = null;
|
|
43
|
-
let firstEventMs = null;
|
|
44
|
-
let lastEventMs = null;
|
|
45
|
-
let cancelled = false;
|
|
46
|
-
|
|
47
|
-
const streamPath = join(runDirectory, "stream.ndjson");
|
|
48
|
-
const stderrPath = join(runDirectory, "stderr.log");
|
|
49
|
-
const responsePath = join(runDirectory, "response.raw.txt");
|
|
50
|
-
|
|
51
|
-
const streamHandle = await openFileHandle(streamPath, "w");
|
|
52
|
-
const stderrHandle = await openFileHandle(stderrPath, "w");
|
|
53
|
-
|
|
54
|
-
const verbose = Boolean(process.env.OFFGRID_BENCHMARK_VERBOSE);
|
|
55
|
-
const renderState = {
|
|
56
|
-
cwd: runDirectory,
|
|
57
|
-
turn: 0,
|
|
58
|
-
turnHadToolError: false,
|
|
59
|
-
modelPrinted: false,
|
|
60
|
-
activeTool: null,
|
|
61
|
-
execTimer: null,
|
|
62
|
-
status: { mode: "idle", toolName: null, bytes: 0, tokens: 0, execStartedAt: null },
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
function appendResponse(text) {
|
|
66
|
-
responseBuffer += text;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function flushResponse() {
|
|
70
|
-
if (responseBuffer) {
|
|
71
|
-
runResult.rawResponseLines.push(responseBuffer);
|
|
72
|
-
responseBuffer = "";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function updateTimeBounds(timestamp) {
|
|
77
|
-
if (!timestamp) return;
|
|
78
|
-
if (firstEventMs === null) firstEventMs = timestamp;
|
|
79
|
-
lastEventMs = timestamp;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function beginTurn() {
|
|
83
|
-
runResult.agentTurns += 1;
|
|
84
|
-
currentTurnStartMs = lastTurnEndMs ?? runStartMs ?? null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function endTurn(usage, timestamp) {
|
|
88
|
-
const turnEndMs = timestamp ?? null;
|
|
89
|
-
const wallClockMs = currentTurnStartMs && turnEndMs ? turnEndMs - currentTurnStartMs : null;
|
|
90
|
-
runResult.perTurn.push({
|
|
91
|
-
turn: runResult.agentTurns,
|
|
92
|
-
inputTokens: usage?.input ?? 0,
|
|
93
|
-
outputTokens: usage?.output ?? 0,
|
|
94
|
-
cacheRead: usage?.cacheRead ?? 0,
|
|
95
|
-
cacheWrite: usage?.cacheWrite ?? 0,
|
|
96
|
-
wallClockMs,
|
|
97
|
-
toolCalls: 0,
|
|
98
|
-
});
|
|
99
|
-
if (turnEndMs) lastTurnEndMs = turnEndMs;
|
|
100
|
-
currentTurnStartMs = null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function processLine(line) {
|
|
104
|
-
if (!line.trim()) return;
|
|
105
|
-
streamHandle.write(line + "\n");
|
|
106
|
-
let parsed;
|
|
107
|
-
try {
|
|
108
|
-
parsed = JSON.parse(line);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
console.log(BENCH_COLORS.error(`[parse error] ${err.message}`));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const timestamp = extractTimestamp(parsed);
|
|
115
|
-
updateTimeBounds(timestamp);
|
|
116
|
-
|
|
117
|
-
renderStreamEvent(parsed, renderState, { verbose });
|
|
118
|
-
|
|
119
|
-
if (parsed.type === "session" || parsed.type === "agent_start") {
|
|
120
|
-
if (timestamp && runStartMs === null) runStartMs = timestamp;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (parsed.type === "turn_start") {
|
|
124
|
-
beginTurn();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (parsed.type === "turn_end" && parsed.message?.usage) {
|
|
128
|
-
const usage = parsed.message.usage;
|
|
129
|
-
runResult.promptTokens += usage.input ?? 0;
|
|
130
|
-
runResult.completionTokens += usage.output ?? 0;
|
|
131
|
-
runResult.cacheRead += usage.cacheRead ?? 0;
|
|
132
|
-
runResult.cacheWrite += usage.cacheWrite ?? 0;
|
|
133
|
-
endTurn(usage, timestamp);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (parsed.type === "message_update" && parsed.assistantMessageEvent) {
|
|
137
|
-
const evt = parsed.assistantMessageEvent;
|
|
138
|
-
const subtype = String(evt.type ?? "").replace(/_/gu, "");
|
|
139
|
-
if (subtype === "thinkingdelta" || subtype === "textdelta") {
|
|
140
|
-
appendResponse(evt.delta || "");
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (parsed.type === "message_end" && parsed.message?.role === "assistant") {
|
|
145
|
-
flushResponse();
|
|
146
|
-
const content = parsed.message.content ?? [];
|
|
147
|
-
for (const item of content) {
|
|
148
|
-
if (item.type === "toolCall") {
|
|
149
|
-
runResult.toolCalls += 1;
|
|
150
|
-
appendResponse(`\n${formatToolCall(item)}\n`);
|
|
151
|
-
const currentTurn = runResult.perTurn[runResult.perTurn.length - 1];
|
|
152
|
-
if (currentTurn) currentTurn.toolCalls += 1;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (parsed.type === "toolResult") {
|
|
158
|
-
runResult.toolResults += 1;
|
|
159
|
-
const status = parsed.isError ? "error" : "ok";
|
|
160
|
-
appendResponse(`\n[toolResult] ${parsed.toolName} (${status})\n`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (parsed.type === "agent_end") {
|
|
164
|
-
flushResponse();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
child.stdout.setEncoding("utf8");
|
|
169
|
-
child.stdout.on("data", (chunk) => {
|
|
170
|
-
streamBuffer += chunk;
|
|
171
|
-
const lines = streamBuffer.split("\n");
|
|
172
|
-
streamBuffer = lines.pop();
|
|
173
|
-
for (const line of lines) {
|
|
174
|
-
processLine(line);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
child.stderr.setEncoding("utf8");
|
|
179
|
-
child.stderr.on("data", (chunk) => {
|
|
180
|
-
stderrHandle.write(chunk);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const abortListener = () => {
|
|
184
|
-
if (cancelled) return;
|
|
185
|
-
cancelled = true;
|
|
186
|
-
console.log(BENCH_COLORS.error("\n\n[Cancelled by user]"));
|
|
187
|
-
child.kill("SIGTERM");
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
if (signal) {
|
|
191
|
-
signal.addEventListener("abort", abortListener);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return new Promise((resolve) => {
|
|
195
|
-
child.on("exit", async (code) => {
|
|
196
|
-
if (signal) signal.removeEventListener("abort", abortListener);
|
|
197
|
-
stopExecTimer(renderState);
|
|
198
|
-
if (streamBuffer.trim()) {
|
|
199
|
-
processLine(streamBuffer);
|
|
200
|
-
}
|
|
201
|
-
flushResponse();
|
|
202
|
-
await streamHandle.close();
|
|
203
|
-
await stderrHandle.close();
|
|
204
|
-
await writeFile(responsePath, runResult.rawResponseLines.join(""), "utf8");
|
|
205
|
-
|
|
206
|
-
runResult.exitCode = code ?? 0;
|
|
207
|
-
if (firstEventMs !== null && lastEventMs !== null) {
|
|
208
|
-
runResult.wallClockMs = lastEventMs - firstEventMs;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (cancelled) {
|
|
212
|
-
runResult.error = { message: "Cancelled by user" };
|
|
213
|
-
resolve(runResult);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
printFinalLine(BENCH_COLORS.info("Pi benchmark finished"));
|
|
218
|
-
|
|
219
|
-
if (runResult.exitCode !== 0) {
|
|
220
|
-
runResult.error = { message: `Pi exited with code ${runResult.exitCode}` };
|
|
221
|
-
resolve(runResult);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
resolve(runResult);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
child.on("error", async (err) => {
|
|
229
|
-
if (signal) signal.removeEventListener("abort", abortListener);
|
|
230
|
-
stopExecTimer(renderState);
|
|
231
|
-
await streamHandle.close();
|
|
232
|
-
await stderrHandle.close();
|
|
233
|
-
runResult.error = { message: err.message };
|
|
234
|
-
resolve(runResult);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function extractTimestamp(event) {
|
|
240
|
-
const raw = event?.message?.timestamp ?? event?.timestamp ?? event?.assistantMessageEvent?.partial?.timestamp;
|
|
241
|
-
if (typeof raw === "number") return raw;
|
|
242
|
-
if (typeof raw === "string") {
|
|
243
|
-
const parsed = Date.parse(raw);
|
|
244
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
245
|
-
}
|
|
246
|
-
const iso = event?.message?.createdAt ?? event?.createdAt ?? event?.created_at;
|
|
247
|
-
if (typeof iso === "string") {
|
|
248
|
-
const parsed = Date.parse(iso);
|
|
249
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function openFileHandle(path, flags) {
|
|
255
|
-
const { open } = await import("node:fs/promises");
|
|
256
|
-
return open(path, flags);
|
|
257
|
-
}
|