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