dsh-livebench-panel 0.2.21 → 0.2.23
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 +5 -1
- package/lib/index.js +99 -7
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -805,10 +805,14 @@ window.__ModuleLoader__.load({
|
|
|
805
805
|
row.errors > 0 ? `${row.errors} 题 API 失败` : null,
|
|
806
806
|
emptyCount > 0 ? `${emptyCount} 题思考占满 max-tokens、未产出答案` : null,
|
|
807
807
|
].filter(Boolean).join(",");
|
|
808
|
+
const missing = Array.isArray(row.missingIndexes) ? row.missingIndexes : null;
|
|
809
|
+
const missingText = missing !== null && missing.length > 0
|
|
810
|
+
? `\n没做出来的题号(0 起,可复制去重跑):${missing.join("、")}`
|
|
811
|
+
: "";
|
|
808
812
|
const title = basePrefix + (allFailed
|
|
809
813
|
? `本次选择 ${row.configured ?? 0} 题,全部没做出来(${reasons}),不计入正确率 ${sub}`
|
|
810
814
|
: `本次选择 ${row.configured ?? 0} 题,做出来 ${row.done ?? 0} 题,没做出来 ${row.notDone ?? 0} 题 ${sub}`
|
|
811
|
-
+ (reasons ? `(其中 ${reasons})` : "")); if (allFailed || row.judged === 0) {
|
|
815
|
+
+ (reasons ? `(其中 ${reasons})` : "")) + missingText; if (allFailed || row.judged === 0) {
|
|
812
816
|
return h("td", { key: taskKeyCol, className: cls, title },
|
|
813
817
|
h("div", { className: c("dim") }, "—"),
|
|
814
818
|
h("div", { className: c("cellSub") }, sub));
|
package/lib/index.js
CHANGED
|
@@ -627,6 +627,73 @@ function isEmptyAnswerLine(line) {
|
|
|
627
627
|
return EMPTY_ANSWER_RE.test(line);
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
/** 从答案行里抽 question_id(只做正则,不 JSON.parse)。 */
|
|
631
|
+
const QUESTION_ID_RE = /"question_id"\s*:\s*"([A-Za-z0-9_.:-]{8,120})"/;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 「题目序号范围」的边界是否真的设了。
|
|
635
|
+
*
|
|
636
|
+
* 注意:面板表单里没填时提交的是**空字符串**,不是 null。旧代码只判 `!== null/undefined`,
|
|
637
|
+
* 于是 `""` 被当成"设了范围",`Number("") === 0` → 范围变成 0..0,只检查第 0 题。
|
|
638
|
+
*/
|
|
639
|
+
function isRangeBoundSet(value) {
|
|
640
|
+
return value !== null && value !== undefined && String(value).trim().length > 0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** 把范围边界规范成 number 或 null(空串一律当没设)。 */
|
|
644
|
+
function normalizeRangeBound(value) {
|
|
645
|
+
return isRangeBoundSet(value) ? Number(value) : null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* 取某次运行的 baseline 有序 id 列表。
|
|
650
|
+
* 新元数据直接读 baselineIds;老元数据(加字段之前启动的运行)从
|
|
651
|
+
* baseline / baselineTask / baselinePicks 反查 BASELINE_SETS 还原。
|
|
652
|
+
* @returns {string[]|null}
|
|
653
|
+
*/
|
|
654
|
+
function baselineIdsFor(meta) {
|
|
655
|
+
if (!meta) return null;
|
|
656
|
+
if (Array.isArray(meta.baselineIds) && meta.baselineIds.length > 0) return meta.baselineIds;
|
|
657
|
+
const set = typeof meta.baseline === "string" ? BASELINE_SETS[meta.baseline] : null;
|
|
658
|
+
const taskDef = set && typeof meta.baselineTask === "string"
|
|
659
|
+
? (set.tasks ?? []).find((t) => t.task === meta.baselineTask) ?? null
|
|
660
|
+
: null;
|
|
661
|
+
if (!taskDef || !Array.isArray(meta.baselinePicks) || meta.baselinePicks.length === 0) return null;
|
|
662
|
+
const merged = [];
|
|
663
|
+
for (const pick of meta.baselinePicks) {
|
|
664
|
+
if (pick !== "passed" && pick !== "failed") continue;
|
|
665
|
+
for (const qid of taskDef[pick]) if (!merged.includes(qid)) merged.push(qid);
|
|
666
|
+
}
|
|
667
|
+
return merged.length > 0 ? merged : null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Baseline 评测里"没做出来的题号"。
|
|
672
|
+
*
|
|
673
|
+
* 「没做出来」= 压根没跑 + 跑了但空答案(思考吃满 max-tokens)+ 跑了但 API 失败,
|
|
674
|
+
* 因为它们都该从正确率分母里排除,也都是用户想补跑的题。
|
|
675
|
+
*
|
|
676
|
+
* 编号是 **该题库内的 0 起下标**,与「题目序号范围」的编号一致,可以直接复制去重跑。
|
|
677
|
+
* 范围先按题库实际题数裁剪 —— 「0-1000」这种越界输入不会把编号算歪(基准永远是实际题数)。
|
|
678
|
+
*
|
|
679
|
+
* @param {object|undefined} meta 运行元数据(baselineIds,或可由 baseline 反查)
|
|
680
|
+
* @param {Set<string>} okIds 答案文件里有**有效答案**的 question_id
|
|
681
|
+
* @returns {number[]|null} 不是 baseline 运行时返回 null
|
|
682
|
+
*/
|
|
683
|
+
function missingBaselineIndexes(meta, okIds) {
|
|
684
|
+
const ids = baselineIdsFor(meta);
|
|
685
|
+
if (ids === null) return null;
|
|
686
|
+
const hasBegin = isRangeBoundSet(meta.begin);
|
|
687
|
+
const hasEnd = isRangeBoundSet(meta.end);
|
|
688
|
+
const from = hasBegin ? Math.max(0, Math.min(ids.length, Number(meta.begin))) : 0;
|
|
689
|
+
const to = hasEnd ? Math.max(0, Math.min(ids.length, Number(meta.end) + 1)) : ids.length;
|
|
690
|
+
const out = [];
|
|
691
|
+
for (let i = from; i < Math.max(from, to); i += 1) {
|
|
692
|
+
if (!okIds.has(ids[i])) out.push(i);
|
|
693
|
+
}
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
|
|
630
697
|
async function readTextFileAsync(path) {
|
|
631
698
|
const fsPromises = await import("node:fs/promises");
|
|
632
699
|
try {
|
|
@@ -668,12 +735,25 @@ async function computeResults(dataDir) {
|
|
|
668
735
|
const text = await readTextFileAsync(join(answerDir, file));
|
|
669
736
|
if (text === null) continue;
|
|
670
737
|
const key = `${model}\u0000${task}`;
|
|
671
|
-
const stat = answers.get(key) ?? { answered: 0, errors: 0, empty: 0 };
|
|
738
|
+
const stat = answers.get(key) ?? { answered: 0, errors: 0, empty: 0, okIds: null };
|
|
739
|
+
// baseline 评测要回答"少做了哪几个题",所以需要收集**有有效答案**的 question_id。
|
|
740
|
+
// 「没做出来」= 压根没跑 + 跑了但空答案 + 跑了但 API 失败,三者都算,
|
|
741
|
+
// 因为它们都该从正确率分母里排除,也都是用户想补跑的题。
|
|
742
|
+
// 只用正则抽 id(不 JSON.parse),对几百 KB~几 MB 的答案文件也够快。
|
|
743
|
+
const runMetaEntry = runMeta.get(model);
|
|
744
|
+
const trackIds = baselineIdsFor(runMetaEntry) !== null;
|
|
745
|
+
if (trackIds && stat.okIds === null) stat.okIds = new Set();
|
|
672
746
|
for (const line of text.split("\n")) {
|
|
673
747
|
if (line.trim().length === 0) continue;
|
|
674
748
|
stat.answered += 1;
|
|
675
|
-
|
|
676
|
-
|
|
749
|
+
const isError = line.includes('"$ERROR$"');
|
|
750
|
+
const isEmpty = !isError && isEmptyAnswerLine(line);
|
|
751
|
+
if (isError) stat.errors += 1;
|
|
752
|
+
else if (isEmpty) stat.empty += 1;
|
|
753
|
+
if (trackIds && !isError && !isEmpty) {
|
|
754
|
+
const idMatch = line.match(QUESTION_ID_RE);
|
|
755
|
+
if (idMatch !== null) stat.okIds.add(idMatch[1]);
|
|
756
|
+
}
|
|
677
757
|
}
|
|
678
758
|
// 空文件(尚未产出答案,或成绩已被删除只留下 0 字节壳)不计入结果:
|
|
679
759
|
// 否则删掉的记录会在下一次扫描时凭“文件名仍在”重新冒出来。
|
|
@@ -740,10 +820,14 @@ async function computeResults(dataDir) {
|
|
|
740
820
|
? Number(meta.baselineSize)
|
|
741
821
|
: null;
|
|
742
822
|
const scopeTotal = baselineTotal ?? byRelease ?? total;
|
|
743
|
-
const hasRange = meta.begin
|
|
823
|
+
const hasRange = isRangeBoundSet(meta.begin) && isRangeBoundSet(meta.end);
|
|
744
824
|
if (hasRange) {
|
|
745
825
|
const len = clampedRangeLength(scopeTotal, Number(meta.begin), Number(meta.end));
|
|
746
826
|
if (len !== null) configured = len;
|
|
827
|
+
} else if (baselineTotal !== null) {
|
|
828
|
+
// baseline 且没设范围 = 整个题库,所以"选择题数"恒等于题库大小,
|
|
829
|
+
// 而不是当前已答题数(否则跑到一半会显示"选择 6 题")
|
|
830
|
+
configured = baselineTotal;
|
|
747
831
|
}
|
|
748
832
|
}
|
|
749
833
|
}
|
|
@@ -753,6 +837,9 @@ async function computeResults(dataDir) {
|
|
|
753
837
|
// 正确率 = 做对的题 / **做出来的题**(API 失败、思考吃满 token 没产出答案的都不进分母)
|
|
754
838
|
const judgedDone = Math.max(0, judgedCount - notProduced);
|
|
755
839
|
const score = entry && judgedDone > 0 ? (entry.sum / judgedDone) * 100 : null;
|
|
840
|
+
// Baseline 评测:算出**具体哪几个题号没做出来**(题号 = 该题库内的 0 起下标,
|
|
841
|
+
// 与「题目序号范围」的编号一致,方便直接复制去重跑)。
|
|
842
|
+
const missingIndexes = missingBaselineIndexes(meta, stat.okIds ?? new Set());
|
|
756
843
|
return {
|
|
757
844
|
model,
|
|
758
845
|
category,
|
|
@@ -768,6 +855,8 @@ async function computeResults(dataDir) {
|
|
|
768
855
|
done,
|
|
769
856
|
configured,
|
|
770
857
|
notDone,
|
|
858
|
+
// baseline 专有:没做出来的题号(0 起,相对该题库)
|
|
859
|
+
missingIndexes,
|
|
771
860
|
// 本次运行的开始时间(ISO);旧数据没有元数据时为 null,前端退回解析 displayName 里的运行戳
|
|
772
861
|
runStartedAt: meta !== undefined && typeof meta.startedAt === "string" ? meta.startedAt : null,
|
|
773
862
|
// 非 null 表示这一行跑的是 Baseline 题库子集,不是该任务全量
|
|
@@ -1080,8 +1169,9 @@ function apply(ctx) {
|
|
|
1080
1169
|
if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true });
|
|
1081
1170
|
writeFileSync(join(metaDir, displayName + ".json"), JSON.stringify({
|
|
1082
1171
|
benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
|
|
1083
|
-
|
|
1084
|
-
|
|
1172
|
+
// 空串要存成 null:否则读取端会把「没填」当成"范围 0..0"
|
|
1173
|
+
begin: normalizeRangeBound(body.begin),
|
|
1174
|
+
end: normalizeRangeBound(body.end),
|
|
1085
1175
|
// 算"本次设定题数"要用 release 过滤后的有效题数来裁剪边界,所以必须记下来
|
|
1086
1176
|
release,
|
|
1087
1177
|
// baseline 评测标出来:成绩表里同一列会混着"全量"和"题库子集"两种分数,
|
|
@@ -1093,6 +1183,8 @@ function apply(ctx) {
|
|
|
1093
1183
|
baselineTask: baselinePick !== null ? baselinePick.task : null,
|
|
1094
1184
|
baselinePicks: baselinePick !== null ? baselinePick.picks : null,
|
|
1095
1185
|
baselineSize: baselinePick !== null ? baselinePick.ids.length : null,
|
|
1186
|
+
// 实际使用的有序 id 列表:/results 靠它算出"少做了哪几题"
|
|
1187
|
+
baselineIds: baselinePick !== null ? baselinePick.ids : null,
|
|
1096
1188
|
startedAt: new Date().toISOString(),
|
|
1097
1189
|
}), "utf8");
|
|
1098
1190
|
} catch { /* 元数据写入失败不影响评测 */ }
|
|
@@ -1434,4 +1526,4 @@ function apply(ctx) {
|
|
|
1434
1526
|
}), `${name}: delete route`);
|
|
1435
1527
|
}
|
|
1436
1528
|
|
|
1437
|
-
export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount, isEmptyAnswerLine };
|
|
1529
|
+
export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount, isEmptyAnswerLine, missingBaselineIndexes, baselineIdsFor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-livebench-panel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
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. Ships a Baseline probe set built from the questions a reference model cannot solve.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "cszr (Vithrive)",
|