dsh-livebench-panel 0.2.2 → 0.2.3
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/lib/index.js +124 -35
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -427,43 +427,103 @@ function readJsonl(path) {
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/**
|
|
431
|
+
* 异步流式读取一个 JSONL 文件,对每行回调。
|
|
432
|
+
* 不整读入内存、不阻塞事件循环 —— coding 类 answer 单行可达数 MB,
|
|
433
|
+
* 同步整读会卡死 node 事件循环(表现为 DSH 断连假死)。
|
|
434
|
+
*/
|
|
435
|
+
async function streamJsonl(path, onRow) {
|
|
436
|
+
const fsPromises = await import("node:fs/promises");
|
|
437
|
+
let handle;
|
|
438
|
+
try {
|
|
439
|
+
handle = await fsPromises.open(path, "r");
|
|
440
|
+
} catch {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const decoder = new TextDecoder();
|
|
445
|
+
let buffer = "";
|
|
446
|
+
const chunkSize = 256 * 1024;
|
|
447
|
+
const buf = Buffer.alloc(chunkSize);
|
|
448
|
+
while (true) {
|
|
449
|
+
// 每读一块就让出事件循环,避免大文件连续占用
|
|
450
|
+
const { bytesRead } = await handle.read(buf, 0, chunkSize, null);
|
|
451
|
+
if (bytesRead === 0) break;
|
|
452
|
+
buffer += decoder.decode(buf.subarray(0, bytesRead), { stream: true });
|
|
453
|
+
let idx;
|
|
454
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
455
|
+
const line = buffer.slice(0, idx);
|
|
456
|
+
buffer = buffer.slice(idx + 1);
|
|
457
|
+
if (line.trim().length > 0) onRow(line);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
buffer += decoder.decode();
|
|
461
|
+
if (buffer.trim().length > 0) onRow(buffer);
|
|
462
|
+
} catch {
|
|
463
|
+
/* 读取中断:尽力返回已读内容 */
|
|
464
|
+
} finally {
|
|
465
|
+
try { await handle.close(); } catch { /* ignore */ }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 从一行 answer JSON 提取 (model_id, 是否 $ERROR$)。不整对象解析时仍需 parse,
|
|
471
|
+
* 但只保留必要字段并立即释放 —— 避免大对象堆积。
|
|
472
|
+
*/
|
|
473
|
+
function parseAnswerLineLight(line) {
|
|
474
|
+
try {
|
|
475
|
+
const row = JSON.parse(line);
|
|
476
|
+
const turns = row.choices?.[0]?.turns;
|
|
477
|
+
const text = Array.isArray(turns) ? String(turns[0] ?? "") : "";
|
|
478
|
+
return { model: typeof row.model_id === "string" ? row.model_id : null, is_error: text.startsWith("$ERROR$"), raw: row };
|
|
479
|
+
} catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
430
484
|
/**
|
|
431
485
|
* Compute mean scores per (model, category, task) from ground-truth judgment
|
|
432
486
|
* files, plus the judged/total question counts per task.
|
|
487
|
+
* 异步流式版:不再同步整读 answer 文件(coding 类单行数 MB,同步读会卡死
|
|
488
|
+
* node 事件循环导致 DSH 断连假死)。
|
|
433
489
|
*/
|
|
434
|
-
function computeResults(dataDir) {
|
|
490
|
+
async function computeResults(dataDir) {
|
|
435
491
|
const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n, time}
|
|
436
492
|
const totals = new Map(); // task -> question count
|
|
437
493
|
const answers = new Map(); // `${model}\u0000${task}` -> {answered, errors}
|
|
494
|
+
const catByTask = new Map(); // task -> category(rows 阶段查分类用)
|
|
438
495
|
if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
|
|
439
496
|
for (const category of readDirSafe(dataDir)) {
|
|
440
497
|
const catDir = join(dataDir, category);
|
|
441
498
|
for (const task of readDirSafe(catDir)) {
|
|
442
499
|
const taskDir = join(catDir, task);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
//
|
|
500
|
+
catByTask.set(task, category);
|
|
501
|
+
let questionCount = 0;
|
|
502
|
+
await streamJsonl(join(taskDir, "question.jsonl"), () => { questionCount += 1; });
|
|
503
|
+
totals.set(task, questionCount);
|
|
504
|
+
// answer 侧:各模型产出了多少答案、其中多少 $ERROR$(API 失败)
|
|
448
505
|
const answerDir = join(taskDir, "model_answer");
|
|
449
506
|
for (const file of readDirSafe(answerDir)) {
|
|
450
507
|
if (!file.endsWith(".jsonl")) continue;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
508
|
+
const fallbackModel = file.slice(0, -".jsonl".length);
|
|
509
|
+
await streamJsonl(join(answerDir, file), (line) => {
|
|
510
|
+
const parsed = parseAnswerLineLight(line);
|
|
511
|
+
if (!parsed) return;
|
|
512
|
+
const model = parsed.model ?? fallbackModel;
|
|
455
513
|
const key = `${model}\u0000${task}`;
|
|
456
514
|
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
457
515
|
stat.answered += 1;
|
|
458
|
-
if (
|
|
516
|
+
if (parsed.is_error) stat.errors += 1;
|
|
459
517
|
answers.set(key, stat);
|
|
460
|
-
}
|
|
518
|
+
});
|
|
461
519
|
}
|
|
462
|
-
|
|
463
|
-
|
|
520
|
+
// judgment 侧:判分与时间戳
|
|
521
|
+
await streamJsonl(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), (line) => {
|
|
522
|
+
let row;
|
|
523
|
+
try { row = JSON.parse(line); } catch { return; }
|
|
464
524
|
const model = typeof row.model === "string" ? row.model : null;
|
|
465
525
|
const score = typeof row.score === "number" ? row.score : Number(row.score);
|
|
466
|
-
if (model === null || !Number.isFinite(score) || score < 0)
|
|
526
|
+
if (model === null || !Number.isFinite(score) || score < 0) return;
|
|
467
527
|
const key = `${model}\u0000${task}`;
|
|
468
528
|
const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
|
|
469
529
|
entry.sum += score;
|
|
@@ -471,7 +531,9 @@ function computeResults(dataDir) {
|
|
|
471
531
|
const tstamp = Number(row.tstamp);
|
|
472
532
|
if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
|
|
473
533
|
judged.set(key, entry);
|
|
474
|
-
}
|
|
534
|
+
});
|
|
535
|
+
// 让出事件循环:任务之间穿插执行,保证 HTTP/websocket 实时响应
|
|
536
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
475
537
|
}
|
|
476
538
|
}
|
|
477
539
|
const keys = new Set([...judged.keys(), ...answers.keys()]);
|
|
@@ -479,15 +541,10 @@ function computeResults(dataDir) {
|
|
|
479
541
|
const entry = judged.get(key);
|
|
480
542
|
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
481
543
|
const [model, task] = key.split("\u0000");
|
|
482
|
-
|
|
483
|
-
if (!category) {
|
|
484
|
-
for (const cat of readDirSafe(dataDir)) {
|
|
485
|
-
if (existsSync(join(dataDir, cat, task))) { category = cat; break; }
|
|
486
|
-
}
|
|
487
|
-
}
|
|
544
|
+
const category = entry?.category ?? catByTask.get(task) ?? "";
|
|
488
545
|
return {
|
|
489
546
|
model,
|
|
490
|
-
category
|
|
547
|
+
category,
|
|
491
548
|
task,
|
|
492
549
|
score: entry && entry.n > 0 ? (entry.sum / entry.n) * 100 : null,
|
|
493
550
|
judged: entry?.n ?? 0,
|
|
@@ -501,6 +558,31 @@ function computeResults(dataDir) {
|
|
|
501
558
|
return { rows, taskTotals };
|
|
502
559
|
}
|
|
503
560
|
|
|
561
|
+
/**
|
|
562
|
+
* 异步统计某模型在指定 bench 范围内的 $ERROR$ 答案数(用于自动补跑判断)。
|
|
563
|
+
* 只扫 body.benchNames 涉及的任务目录,流式读取不阻塞事件循环。
|
|
564
|
+
*/
|
|
565
|
+
async function countErrorAnswersAsync(dataDir, displayName, body) {
|
|
566
|
+
if (!/^[A-Za-z0-9._@-]{1,160}$/.test(displayName)) return 0;
|
|
567
|
+
const benches = Array.isArray(body?.benchNames) ? body.benchNames : [];
|
|
568
|
+
let errors = 0;
|
|
569
|
+
for (const bn of benches) {
|
|
570
|
+
const parts = bn.split("/").filter((s) => s.length > 0); // live_bench/<cat>[/<task>]
|
|
571
|
+
if (parts.length < 2) continue;
|
|
572
|
+
const category = parts[1];
|
|
573
|
+
const tasks = parts.length >= 3 ? [parts[2]] : readDirSafe(join(dataDir, category));
|
|
574
|
+
for (const task of tasks) {
|
|
575
|
+
if (!/^[A-Za-z0-9_]{1,80}$/.test(task)) continue;
|
|
576
|
+
const file = join(dataDir, category, task, "model_answer", displayName + ".jsonl");
|
|
577
|
+
await streamJsonl(file, (line) => {
|
|
578
|
+
const parsed = parseAnswerLineLight(line);
|
|
579
|
+
if (parsed && parsed.is_error) errors += 1;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return errors;
|
|
584
|
+
}
|
|
585
|
+
|
|
504
586
|
/**
|
|
505
587
|
* Remove every JSONL line of one model from a file (matched on the given
|
|
506
588
|
* field), rewriting the file only when something changed.
|
|
@@ -740,18 +822,25 @@ function apply(ctx) {
|
|
|
740
822
|
record.exitCode = code === null ? -1 : code;
|
|
741
823
|
record.log.push(`[exit ${record.exitCode}]`);
|
|
742
824
|
// 自动补跑:正常结束但存在 $ERROR$ 答案时,用 --resume --retry-failures
|
|
743
|
-
// 只重跑失败题(最多 2
|
|
825
|
+
// 只重跑失败题(最多 2 轮)。统计改为异步流式(同步版曾阻塞事件循环,
|
|
826
|
+
// 且函数缺失会在 close 回调抛未捕获异常 → DSH 假死断连)。
|
|
744
827
|
if (record.exitCode === 0 && record.autoRetries < 2 && record.body) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
828
|
+
(async () => {
|
|
829
|
+
try {
|
|
830
|
+
const errors = await countErrorAnswersAsync(layout.dataDir, record.displayName, record.body);
|
|
831
|
+
if (errors > 0) {
|
|
832
|
+
record.log.push(`[自动补跑] 检测到 ${errors} 条失败答案,自动重试(第 ${record.autoRetries + 1}/2 轮)`);
|
|
833
|
+
const retryBody = { ...record.body, resume: true, retryFailures: true };
|
|
834
|
+
setTimeout(() => {
|
|
835
|
+
startRun(retryBody, record.autoRetries + 1).catch((e) => {
|
|
836
|
+
appendLog(record, `[自动补跑启动失败] ${e.message}`);
|
|
837
|
+
});
|
|
838
|
+
}, 3000);
|
|
839
|
+
}
|
|
840
|
+
} catch (e) {
|
|
841
|
+
record.log.push(`[自动补跑检查失败] ${e.message}`);
|
|
842
|
+
}
|
|
843
|
+
})();
|
|
755
844
|
}
|
|
756
845
|
});
|
|
757
846
|
|
|
@@ -920,7 +1009,7 @@ function apply(ctx) {
|
|
|
920
1009
|
return;
|
|
921
1010
|
}
|
|
922
1011
|
const layout = livebenchLayout();
|
|
923
|
-
const { rows, taskTotals } = computeResults(layout.dataDir);
|
|
1012
|
+
const { rows, taskTotals } = await computeResults(layout.dataDir);
|
|
924
1013
|
sendJson(res, 200, { ok: true, rows, taskTotals });
|
|
925
1014
|
},
|
|
926
1015
|
}), `${name}: results route`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-livebench-panel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "DSH web plugin: a LiveBench tab in the Trajectory view (right of 对话/轨迹). Run LiveBench evaluations against every model configured in the DeepSeek Harness — pick provider/model, category, task, release and question range from dropdowns, watch progress, and read scores in place.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|