dsh-livebench-panel 0.2.10 → 0.2.12
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/client.js +22 -15
- package/lib/index.js +126 -41
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -587,21 +587,28 @@ window.__ModuleLoader__.load({
|
|
|
587
587
|
h("td", null, model),
|
|
588
588
|
matrix.tasks.map((taskKeyCol) => {
|
|
589
589
|
const row = matrix.cells.get(`${model}__@__${taskKeyCol}`);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
title = "有答案但尚未判分";
|
|
599
|
-
} else {
|
|
600
|
-
cellText = `${row.score.toFixed(1)} (${row.judged}/${row.total})`;
|
|
601
|
-
if (row.errors > 0) title += ` · 其中 ${row.errors} 题 API 失败计 0 分`;
|
|
602
|
-
}
|
|
590
|
+
// 单元格:上方正确率,下方 (-没做/设定/总)。
|
|
591
|
+
// 没做/没做完(访问失败、网络中断)不计入正确率分母。
|
|
592
|
+
const subStyle = { fontSize: "10.5px", color: "var(--dsw-alias-label-tertiary)", fontWeight: "400" };
|
|
593
|
+
const sub = `(-${row ? (row.notDone ?? 0) : 0}/${row ? (row.configured ?? 0) : 0}/${row ? row.total : 0})`;
|
|
594
|
+
if (!row) {
|
|
595
|
+
return h("td", { key: taskKeyCol, className: c("score"), title: "该模型未执行此任务(无任何记录)" },
|
|
596
|
+
h("div", null, "×"),
|
|
597
|
+
h("div", { style: subStyle }, sub));
|
|
603
598
|
}
|
|
604
|
-
|
|
599
|
+
const allFailed = row.answered > 0 && row.errors >= row.answered;
|
|
600
|
+
const title = allFailed
|
|
601
|
+
? `该任务 ${row.errors} 题全部访问失败(未做完),不计入正确率`
|
|
602
|
+
: `做完 ${row.done ?? 0} 题,没做/失败 ${row.notDone ?? 0} 题` +
|
|
603
|
+
(row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "");
|
|
604
|
+
if (allFailed || row.judged === 0) {
|
|
605
|
+
return h("td", { key: taskKeyCol, className: c("score"), title },
|
|
606
|
+
h("div", null, "—"),
|
|
607
|
+
h("div", { style: subStyle }, sub));
|
|
608
|
+
}
|
|
609
|
+
return h("td", { key: taskKeyCol, className: c("score"), title },
|
|
610
|
+
h("div", null, row.score.toFixed(1)),
|
|
611
|
+
h("div", { style: subStyle }, sub));
|
|
605
612
|
}),
|
|
606
613
|
h("td", null, h("button", {
|
|
607
614
|
className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
|
|
@@ -658,7 +665,7 @@ window.__ModuleLoader__.load({
|
|
|
658
665
|
"提示:回答为 $ERROR$ 表示该题 API 调用失败(网络/鉴权/参数问题)计 0 分,可在上方日志区查看具体错误;zebra_puzzle(逻辑谜题)是公认最难的任务,低分属正常现象。"),
|
|
659
666
|
),
|
|
660
667
|
h("p", { className: c("hint") },
|
|
661
|
-
"
|
|
668
|
+
"格式说明:单元格上方为正确率(= 做对 / 做完),下方括号 (-没做/设定/总):没做或没做完的题(访问失败、网络中断、未跑到)不计入正确率。× 表示该模型完全没跑过该任务。正式榜单可用 release 2024-11-25。"),
|
|
662
669
|
);
|
|
663
670
|
}
|
|
664
671
|
/**
|
package/lib/index.js
CHANGED
|
@@ -307,6 +307,22 @@ function readDirSafe(dir) {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/** 列出目录下的【文件】(可按扩展名过滤)——注意 readDirSafe 只列目录。 */
|
|
311
|
+
function readFilesSafe(dir, ext) {
|
|
312
|
+
try {
|
|
313
|
+
return readdirSync(dir).filter((f) => {
|
|
314
|
+
try {
|
|
315
|
+
if (!statSync(join(dir, f)).isFile()) return false;
|
|
316
|
+
return ext === undefined || f.endsWith(ext);
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
} catch {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
310
326
|
function readdirNames(dir) {
|
|
311
327
|
try {
|
|
312
328
|
return readdirSync(dir).filter((entry) => {
|
|
@@ -485,56 +501,83 @@ function parseAnswerLineLight(line) {
|
|
|
485
501
|
|
|
486
502
|
/**
|
|
487
503
|
* Compute mean scores per (model, category, task) from ground-truth judgment
|
|
488
|
-
* files, plus
|
|
489
|
-
*
|
|
490
|
-
*
|
|
504
|
+
* files, plus judged/total question counts per task.
|
|
505
|
+
*
|
|
506
|
+
* 性能:数据目录可能上百 MB(每次评测生成独立答案文件)。这里:
|
|
507
|
+
* - answer 文件用【文件名】当模型名,只做行数与 "$ERROR$" 子串统计 —— 不解析大 JSON 行;
|
|
508
|
+
* - question.jsonl 只数行数;
|
|
509
|
+
* - 结果缓存 5 秒,避免前端频繁刷新时重复全量扫描。
|
|
491
510
|
*/
|
|
511
|
+
let resultsCache = { at: 0, data: null };
|
|
512
|
+
|
|
513
|
+
async function readTextFileAsync(path) {
|
|
514
|
+
const fsPromises = await import("node:fs/promises");
|
|
515
|
+
try {
|
|
516
|
+
return await fsPromises.readFile(path, "utf8");
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
492
522
|
async function computeResults(dataDir) {
|
|
523
|
+
const now = Date.now();
|
|
524
|
+
if (resultsCache.data !== null && now - resultsCache.at < 5000) return resultsCache.data;
|
|
493
525
|
const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n, time}
|
|
494
526
|
const totals = new Map(); // task -> question count
|
|
495
527
|
const answers = new Map(); // `${model}\u0000${task}` -> {answered, errors}
|
|
496
|
-
const catByTask = new Map();
|
|
528
|
+
const catByTask = new Map();
|
|
497
529
|
if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
|
|
530
|
+
// 读取每次评测的配置范围(displayName -> {benchNames, begin, end})
|
|
531
|
+
const runMeta = new Map();
|
|
532
|
+
try {
|
|
533
|
+
const metaDir = join(dataDir, ".dsh_runs");
|
|
534
|
+
for (const f of readdirSync(metaDir)) {
|
|
535
|
+
if (!f.endsWith(".json")) continue;
|
|
536
|
+
try {
|
|
537
|
+
runMeta.set(f.slice(0, -5), JSON.parse(readFileSync(join(metaDir, f), "utf8")));
|
|
538
|
+
} catch { /* 单个元数据损坏忽略 */ }
|
|
539
|
+
}
|
|
540
|
+
} catch { /* 无元数据目录(历史数据) */ }
|
|
498
541
|
for (const category of readDirSafe(dataDir)) {
|
|
499
542
|
const catDir = join(dataDir, category);
|
|
500
543
|
for (const task of readDirSafe(catDir)) {
|
|
501
544
|
const taskDir = join(catDir, task);
|
|
502
545
|
catByTask.set(task, category);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
totals.set(task, questionCount);
|
|
506
|
-
// answer 侧:各模型产出了多少答案、其中多少 $ERROR$(API 失败)
|
|
546
|
+
const qText = await readTextFileAsync(join(taskDir, "question.jsonl"));
|
|
547
|
+
totals.set(task, qText === null ? 0 : qText.split("\n").reduce((n, l) => n + (l.trim().length > 0 ? 1 : 0), 0));
|
|
507
548
|
const answerDir = join(taskDir, "model_answer");
|
|
508
|
-
for (const file of
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
549
|
+
for (const file of readFilesSafe(answerDir, ".jsonl")) {
|
|
550
|
+
const model = file.slice(0, -".jsonl".length);
|
|
551
|
+
const text = await readTextFileAsync(join(answerDir, file));
|
|
552
|
+
if (text === null) continue;
|
|
553
|
+
const key = `${model}\u0000${task}`;
|
|
554
|
+
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
555
|
+
for (const line of text.split("\n")) {
|
|
556
|
+
if (line.trim().length === 0) continue;
|
|
517
557
|
stat.answered += 1;
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
558
|
+
if (line.includes('"$ERROR$"')) stat.errors += 1;
|
|
559
|
+
}
|
|
560
|
+
answers.set(key, stat);
|
|
521
561
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
562
|
+
const jText = await readTextFileAsync(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
|
|
563
|
+
if (jText !== null) {
|
|
564
|
+
for (const line of jText.split("\n")) {
|
|
565
|
+
if (line.trim().length === 0) continue;
|
|
566
|
+
let row;
|
|
567
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
568
|
+
const model = typeof row.model === "string" ? row.model : null;
|
|
569
|
+
const score = typeof row.score === "number" ? row.score : Number(row.score);
|
|
570
|
+
if (model === null || !Number.isFinite(score) || score < 0) continue;
|
|
571
|
+
const key = `${model}\u0000${task}`;
|
|
572
|
+
const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
|
|
573
|
+
entry.sum += score;
|
|
574
|
+
entry.n += 1;
|
|
575
|
+
const tstamp = Number(row.tstamp);
|
|
576
|
+
if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
|
|
577
|
+
judged.set(key, entry);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// 让出事件循环,保证 HTTP/websocket 实时响应
|
|
538
581
|
await new Promise((resolve) => setImmediate(resolve));
|
|
539
582
|
}
|
|
540
583
|
}
|
|
@@ -544,20 +587,48 @@ async function computeResults(dataDir) {
|
|
|
544
587
|
const stat = answers.get(key) ?? { answered: 0, errors: 0 };
|
|
545
588
|
const [model, task] = key.split("\u0000");
|
|
546
589
|
const category = entry?.category ?? catByTask.get(task) ?? "";
|
|
590
|
+
const total = totals.get(task) ?? 0;
|
|
591
|
+
const judgedCount = entry?.n ?? 0;
|
|
592
|
+
// 做完的题 = 实际产出的有效答案(尝试数 - 访问失败数)
|
|
593
|
+
const done = Math.max(0, stat.answered - stat.errors);
|
|
594
|
+
// 设定题数:优先用评测元数据的范围(end 含端点),否则用尝试/判分中的较大值
|
|
595
|
+
let configured = Math.max(stat.answered, judgedCount);
|
|
596
|
+
const meta = runMeta.get(model);
|
|
597
|
+
if (meta !== undefined) {
|
|
598
|
+
const inScope = (meta.benchNames || []).some((bn) => {
|
|
599
|
+
const parts = String(bn).split("/").filter((s) => s.length > 0);
|
|
600
|
+
if (parts.length >= 3) return parts[1] === category && parts[2] === task;
|
|
601
|
+
if (parts.length === 2) return parts[1] === category;
|
|
602
|
+
return true;
|
|
603
|
+
});
|
|
604
|
+
if (inScope) {
|
|
605
|
+
const hasRange = meta.begin !== null && meta.begin !== undefined && meta.end !== null && meta.end !== undefined;
|
|
606
|
+
configured = hasRange ? Math.min(total, Number(meta.end) - Number(meta.begin) + 1) : total;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (configured < stat.answered) configured = stat.answered;
|
|
610
|
+
const notDone = Math.max(0, configured - done);
|
|
611
|
+
// 正确率 = 做对的题 / 已判分的做完题(访问失败/未做的题不计入分母)
|
|
612
|
+
const judgedDone = Math.max(0, judgedCount - stat.errors);
|
|
613
|
+
const score = entry && judgedDone > 0 ? (entry.sum / judgedDone) * 100 : null;
|
|
547
614
|
return {
|
|
548
615
|
model,
|
|
549
616
|
category,
|
|
550
617
|
task,
|
|
551
|
-
score
|
|
618
|
+
score,
|
|
552
619
|
judged: entry?.n ?? 0,
|
|
553
|
-
total
|
|
620
|
+
total,
|
|
554
621
|
time: entry && entry.time > 0 ? entry.time : null,
|
|
555
622
|
answered: stat.answered,
|
|
556
623
|
errors: stat.errors,
|
|
624
|
+
done,
|
|
625
|
+
configured,
|
|
626
|
+
notDone,
|
|
557
627
|
};
|
|
558
628
|
});
|
|
559
|
-
const
|
|
560
|
-
|
|
629
|
+
const result = { rows, taskTotals: Object.fromEntries(totals) };
|
|
630
|
+
resultsCache = { at: Date.now(), data: result };
|
|
631
|
+
return result;
|
|
561
632
|
}
|
|
562
633
|
|
|
563
634
|
/**
|
|
@@ -790,6 +861,19 @@ function apply(ctx) {
|
|
|
790
861
|
}
|
|
791
862
|
}
|
|
792
863
|
|
|
864
|
+
// 记录本次评测的配置范围:/results 用它计算每个任务的"设定题数"
|
|
865
|
+
// (未做/失败的题数 = 设定题数 - 实际产出的有效答案数)。
|
|
866
|
+
try {
|
|
867
|
+
const metaDir = join(layout.dataDir, ".dsh_runs");
|
|
868
|
+
if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true });
|
|
869
|
+
writeFileSync(join(metaDir, displayName + ".json"), JSON.stringify({
|
|
870
|
+
benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
|
|
871
|
+
begin: body.begin ?? null,
|
|
872
|
+
end: body.end ?? null,
|
|
873
|
+
startedAt: new Date().toISOString(),
|
|
874
|
+
}), "utf8");
|
|
875
|
+
} catch { /* 元数据写入失败不影响评测 */ }
|
|
876
|
+
|
|
793
877
|
const child = spawn(layout.pythonExe, args, {
|
|
794
878
|
cwd: layout.livebenchDir,
|
|
795
879
|
env,
|
|
@@ -847,7 +931,7 @@ function apply(ctx) {
|
|
|
847
931
|
startRun(retryBody, record.autoRetries + 1).catch((e) => {
|
|
848
932
|
appendLog(record, `[自动补跑启动失败] ${e.message}`);
|
|
849
933
|
});
|
|
850
|
-
}, 15000);
|
|
934
|
+
}, [15000, 60000, 180000][Math.min(record.autoRetries, 2)]);
|
|
851
935
|
}
|
|
852
936
|
} catch (e) {
|
|
853
937
|
record.log.push(`[自动补跑检查失败] ${e.message}`);
|
|
@@ -1101,6 +1185,7 @@ function apply(ctx) {
|
|
|
1101
1185
|
removed += filterJsonlByModel(join(taskDir, "model_answer", `${model}.jsonl`), "model_id", model);
|
|
1102
1186
|
removed += filterJsonlByModel(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), "model", model);
|
|
1103
1187
|
}
|
|
1188
|
+
resultsCache = { at: 0, data: null };
|
|
1104
1189
|
sendJson(res, 200, { ok: true, removed });
|
|
1105
1190
|
},
|
|
1106
1191
|
}), `${name}: delete 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.12",
|
|
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",
|