dsh-livebench-panel 0.2.18 → 0.2.21

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/README.md CHANGED
@@ -42,11 +42,43 @@ cd livebench
42
42
  | 模型 | harness 全部 provider 的全部模型(含内置 deepseek-official);分组展示 | 首次验证选小任务 + 便宜模型 |
43
43
  | 推理强度 | 该模型的 reasoning effort(无配置则禁用);编码进条目名,不同强度独立出分 | 日常 `medium`/`high`;对比测试固定同一档 |
44
44
  | 题集 release | LiveBench 题目发布批次 | 公开题目最全的是 `2024-11-25`(推荐) |
45
+ | **Baseline 题库** | 参考模型的实测对错题集,**可脱离分类/任务单独使用**(见下节) | 快速探针:参考模型「做错」的题 |
45
46
  | 分类 | 六大类:coding / math / reasoning / language / data_analysis / instruction_following | 首次验证选 `language` |
46
47
  | 任务 | 分类下的具体任务(如 language/typos 拼写纠错) | 首次验证选 `typos`(短平快) |
47
- | 题目序号范围 | 该任务题目的起止下标(0 起,**含首尾**) | 冒烟测试填 0–1(只跑 2 题) |
48
+ | 题目序号范围 | 起止下标(0 起,**含首尾**);选 Baseline 时序号相对该题库 | 冒烟测试填 0–1(只跑 2 题) |
48
49
  | max-tokens | 单次回答的 token 上限 | 推理模型给 8192+,否则思考被截断判 0 分 |
49
50
 
51
+ ## Baseline 题库(探针)
52
+
53
+ 用**一个参考模型的实测对错**当尺子,快速判断别的模型处在哪个档位。位于「分类 / 任务」之上,
54
+ 三层可选:
55
+
56
+ 1. **参考模型**:`Baseline(glm-5.3-flash@max)`
57
+ 2. **任务**:该参考模型已经跑过全量的任务(如 `olympiad(36)`)
58
+ 3. **做错 / 做对**:各自标注题目数量(如 `做错(8)` / `做对(28)`),可多选
59
+
60
+ 选中后**不需要也不应该再选分类/任务**——"不选 = 全部分类"这条规则对 Baseline 不适用。
61
+ 「题目序号范围」仍然生效,序号相对 Baseline 题库(0 起、含首尾);超出上界不会报错,
62
+ 会按实际题数截断。
63
+
64
+ **为什么默认勾「做错」**:探针的价值在于"参考模型做不出来的题"。参考模型能轻松做对的题,
65
+ 对更强的模型没有分辨力,混进来只会稀释信号。「做对」也保留,用于反向验证
66
+ (例如确认某模型确实强于参考模型)。
67
+
68
+ **当前内置的数据**(2026-09-12 实测,release `2024-11-25`):
69
+
70
+ | 参考模型 | 任务 | 全量 | 做对 | 做错 |
71
+ |---|---|---|---|---|
72
+ | `zai-coding-cn__glm-5.3-flash@max` | `math/olympiad` | 36 | 28 | 8 |
73
+
74
+ 实测效果(跑「做错」那 8 题):`aiportx-gg/grok-4.6@xhigh` 明显强于参考模型;
75
+ `deepseek-official/deepseek-flash@max` 明显更弱。失败的 8 题全部属于 olympiad 的
76
+ **公式还原**题型——把解答里的公式挖空让模型补全,是纯符号推理,很吃"智力"。
77
+
78
+ 后续任务按同样方式补进 `lib/index.js` 的 `BASELINE_SETS.tasks` 即可:
79
+ 只需填 `passed` / `failed` 两组 question id(取自 LiveBench 的 `question.jsonl`),
80
+ 面板用 `--question-id` 直接指定,题目在题库里是否连续都无所谓。
81
+
50
82
  ## 功能
51
83
 
52
84
  - **标签页位置**:`conversation.view` 槽位 `id: "livebench"`、`order: 20`(对话 = 0,轨迹 = 10,LiveBench 排最右)。
package/lib/client.js CHANGED
@@ -121,7 +121,7 @@ window.__ModuleLoader__.load({
121
121
  function LiveBenchView() {
122
122
  const [config, setConfig] = useState(null);
123
123
  const [configError, setConfigError] = useState(null);
124
- const [sel, setSel] = useState({ models: [], efforts: {}, cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000" });
124
+ const [sel, setSel] = useState({ models: [], efforts: {}, cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000", baseline: null });
125
125
  const [modelDdOpen, setModelDdOpen] = useState(false);
126
126
  const [busy, setBusy] = useState(false);
127
127
  const [running, setRunning] = useState(false);
@@ -131,8 +131,8 @@ window.__ModuleLoader__.load({
131
131
  const [homeBusy, setHomeBusy] = useState(false);
132
132
  const [results, setResults] = useState(null);
133
133
  const [pickedModels, setPickedModels] = useState(() => new Set());
134
- const [sortBy, setSortBy] = useState(null);
135
- const [sortDir, setSortDir] = useState("asc");
134
+ const [sortBy, setSortBy] = useState("time");
135
+ const [sortDir, setSortDir] = useState("desc");
136
136
  const [modelOrder, setModelOrder] = useState(() => {
137
137
  try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
138
138
  });
@@ -243,24 +243,54 @@ window.__ModuleLoader__.load({
243
243
  return { providerId: pid, modelId: mid, modelValue: value, efforts: model?.efforts ?? [] };
244
244
  }), [sel.models, config]);
245
245
 
246
+ // Baseline 三层选择:参考模型 -> 任务 -> 做对/做错
247
+ const baselineObj = useMemo(
248
+ () => (config?.baselines ?? []).find((b) => b.id === sel.baseline) ?? null,
249
+ [config, sel.baseline]);
250
+ const baselineTaskObj = useMemo(
251
+ () => (baselineObj?.tasks ?? []).find((t) => t.task === sel.baselineTask) ?? null,
252
+ [baselineObj, sel.baselineTask]);
253
+ // 当前勾选真正会跑几题:题号范围的上界就是它 - 1
254
+ const baselineSelectedCount = useMemo(() => {
255
+ if (baselineTaskObj === null) return 0;
256
+ let n = 0;
257
+ if (sel.baselinePicks.includes("failed")) n += baselineTaskObj.failedCount;
258
+ if (sel.baselinePicks.includes("passed")) n += baselineTaskObj.passedCount;
259
+ return n;
260
+ }, [baselineTaskObj, sel.baselinePicks]);
261
+
246
262
  const onStart = async () => {
247
263
  setStartError(null);
248
264
  if (selectedModelEntries.length === 0) {
249
265
  setStartError("请先在模型下拉中至少勾选一个模型。");
250
266
  return;
251
267
  }
252
- const benchNames = [];
253
- if (sel.cats.length === 0) {
254
- benchNames.push("live_bench");
255
- } else if (sel.tasks.length === 0) {
256
- for (const cat of sel.cats) benchNames.push(`live_bench/${cat}`);
268
+ // Baseline 题库:绕开分类/任务的"不选 = 全部"推导,
269
+ // 由服务端按「参考模型 + 任务 + 做对/做错」解析出 question id。
270
+ let benchNames = null;
271
+ if (baselineObj !== null) {
272
+ if (baselineTaskObj === null) {
273
+ setStartError("已选 Baseline,请再选择它下面的任务(例如 olympiad)。");
274
+ return;
275
+ }
276
+ if (sel.baselinePicks.length === 0) {
277
+ setStartError("已选 Baseline 任务,请至少勾选「做错」或「做对」之一。");
278
+ return;
279
+ }
257
280
  } else {
258
- for (const t of sel.tasks) benchNames.push(`live_bench/${t}`);
259
- }
260
- const zeroTask = sel.tasks.find((t) => countFor(t.split("/")[0], t.split("/")[1]) === 0);
261
- if (zeroTask) {
262
- setStartError(`任务 ${zeroTask.split("/")[1]} release ${sel.release} 下没有可用题目(该批题目已退役),请取消勾选或换 release。`);
263
- return;
281
+ benchNames = [];
282
+ if (sel.cats.length === 0) {
283
+ benchNames.push("live_bench");
284
+ } else if (sel.tasks.length === 0) {
285
+ for (const cat of sel.cats) benchNames.push(`live_bench/${cat}`);
286
+ } else {
287
+ for (const t of sel.tasks) benchNames.push(`live_bench/${t}`);
288
+ }
289
+ const zeroTask = sel.tasks.find((t) => countFor(t.split("/")[0], t.split("/")[1]) === 0);
290
+ if (zeroTask) {
291
+ setStartError(`任务 ${zeroTask.split("/")[1]} 在 release ${sel.release} 下没有可用题目(该批题目已退役),请取消勾选或换 release。`);
292
+ return;
293
+ }
264
294
  }
265
295
  setBusy(true);
266
296
  try {
@@ -274,7 +304,9 @@ window.__ModuleLoader__.load({
274
304
  provider: entry.providerId,
275
305
  model: entry.modelId,
276
306
  reasoningEffort: sel.efforts[entry.modelValue] ?? "default",
277
- benchNames,
307
+ ...(baselineObj !== null
308
+ ? { baseline: baselineObj.id, baselineTask: sel.baselineTask, baselinePicks: sel.baselinePicks }
309
+ : { benchNames }),
278
310
  release: sel.release,
279
311
  begin: sel.begin,
280
312
  end: sel.end,
@@ -359,15 +391,20 @@ window.__ModuleLoader__.load({
359
391
  const groupStart = new Set(groups.map((g) => g.tasks[0]));
360
392
  const cells = new Map();
361
393
  const startTimes = new Map();
394
+ // 哪些 模型×任务 是 Baseline 子集跑出来的:同一列里会混着"全量"和
395
+ // "题库子集"两种分数,必须在界面上标出来,否则没法解读。
396
+ const baselineCells = new Set();
362
397
  for (const row of results.rows) {
363
- cells.set(`${row.model}__@__${taskKey(row.category, row.task)}`, row);
398
+ const key = `${row.model}__@__${taskKey(row.category, row.task)}`;
399
+ cells.set(key, row);
400
+ if (row.baselineLabel) baselineCells.add(key);
364
401
  const fromMeta = row.runStartedAt ? Math.floor(Date.parse(row.runStartedAt) / 1000) : null;
365
402
  const candidate = (Number.isFinite(fromMeta) ? fromMeta : null) ?? runStampSeconds(row.model) ?? row.time ?? null;
366
403
  const prev = startTimes.get(row.model);
367
404
  if (candidate && (prev === undefined || candidate < prev)) startTimes.set(row.model, candidate);
368
405
  }
369
406
  const taskTotals = (results && results.taskTotals) || {};
370
- return { models, tasks, groups, groupStart, cells, startTimes, taskTotals };
407
+ return { models, tasks, groups, groupStart, cells, startTimes, taskTotals, baselineCells };
371
408
  }, [results]);
372
409
  const cellOf = (model, key) => matrix.cells.get(`${model}__@__${key}`);
373
410
  // 排序:sortBy=null 时用拖拽自定义顺序;点击表头在 正序/倒序 间切换
@@ -380,8 +417,13 @@ window.__ModuleLoader__.load({
380
417
  }
381
418
  if (sortBy === "time") {
382
419
  base.sort((a, b) => {
420
+ // 没有时间戳的排在最后,不管升序降序 —— 否则空值会被当成 0
421
+ // 混在中间/开头,看起来像"最旧的一次评测"。
383
422
  const ta = matrix.startTimes.get(a) ?? 0;
384
423
  const tb = matrix.startTimes.get(b) ?? 0;
424
+ if (ta === 0 && tb === 0) return a.localeCompare(b);
425
+ if (ta === 0) return 1;
426
+ if (tb === 0) return -1;
385
427
  return sortDir === "asc" ? ta - tb : tb - ta;
386
428
  });
387
429
  return base;
@@ -398,7 +440,7 @@ window.__ModuleLoader__.load({
398
440
  // 点击表头:切换排序字段(再次点击同一列则反转方向)
399
441
  const toggleSort = (field) => {
400
442
  if (sortBy === field) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
401
- else { setSortBy(field); setSortDir("asc"); }
443
+ else { setSortBy(field); setSortDir(field === "time" ? "desc" : "asc"); }
402
444
  };
403
445
  const sortMark = (field) => (sortBy === field ? (sortDir === "asc" ? " ↑" : " ↓") : "");
404
446
 
@@ -536,7 +578,7 @@ window.__ModuleLoader__.load({
536
578
  ),
537
579
  h("div", { className: c("field") },
538
580
  h("label", { className: c("label") }, "max-tokens(默认 32000)"),
539
- h("input", { className: c("input"), type: "number", min: 256, max: 32768, value: sel.maxTokens, onChange: setField("maxTokens") }),
581
+ h("input", { className: c("input"), type: "number", min: 256, max: 200000, value: sel.maxTokens, onChange: setField("maxTokens"), title: "单题输出上限。推理模型在难题上容易把预算全花在思考里、正文为空(成绩表括号里记为「没做出来」),这类题可以把上限调大后重跑。" }),
540
582
  ),
541
583
  h("div", { className: c("field") },
542
584
  h("label", { className: c("label") }, "题目序号范围(可选,0 起,含首尾)"),
@@ -544,6 +586,67 @@ window.__ModuleLoader__.load({
544
586
  h("input", { className: c("input"), type: "number", min: 0, placeholder: "起(含)", title: "起始题号(0 起,包含该题)", value: sel.begin, onChange: setField("begin"), style: { flex: "1", minWidth: "0" } }),
545
587
  h("input", { className: c("input"), type: "number", min: 0, placeholder: "止(含)", title: "结束题号(包含该题);只填起不填止 = 从该题跑到底", value: sel.end, onChange: setField("end"), style: { flex: "1", minWidth: "0" } }),
546
588
  ),
589
+ // Baseline 题库:位于「分类 / 任务」之上的独立条目,三层选择。
590
+ // 选中后走它自己的任务 + 一组 question id,因此不需要(也不应该)
591
+ // 再受"不选分类 = 全部分类"这条规则约束。
592
+ h("div", { className: c("field") },
593
+ h("label", { className: c("label") }, "Baseline 题库(探针,可单独使用,不必选分类/任务)"),
594
+ // 第 1 层:参考模型
595
+ h("div", { className: c("chips") },
596
+ (config?.baselines ?? []).map((b) => h("button", {
597
+ key: b.id,
598
+ type: "button",
599
+ className: c("chip") + (sel.baseline === b.id ? " " + c("chipOn") : ""),
600
+ title: `参考模型 ${b.referenceModel}\n已实测 ${b.tasks.length} 个任务`,
601
+ onClick: () => setSel((prev) => (prev.baseline === b.id
602
+ ? { ...prev, baseline: null, baselineTask: null, baselinePicks: [] }
603
+ : { ...prev, baseline: b.id, baselineTask: null, baselinePicks: [] })),
604
+ }, b.label)),
605
+ ),
606
+ // 第 2 层:任务(该参考模型已实测过的任务)
607
+ baselineObj !== null && h("div", { className: c("chips"), style: { marginTop: "6px" } },
608
+ h("span", { className: c("hint"), style: { alignSelf: "center" } }, "任务:"),
609
+ (baselineObj.tasks ?? []).map((t) => h("button", {
610
+ key: t.task,
611
+ type: "button",
612
+ className: c("chip") + (sel.baselineTask === t.task ? " " + c("chipOn") : ""),
613
+ title: `${t.task} · release ${t.release}\n该任务全量 ${t.total} 题(做对 ${t.passedCount} / 做错 ${t.failedCount})`,
614
+ onClick: () => setSel((prev) => (prev.baselineTask === t.task
615
+ ? { ...prev, baselineTask: null, baselinePicks: [] }
616
+ // 默认勾「做错」:探针的用途就是拿参考模型做不出来的题去量别人
617
+ : { ...prev, baselineTask: t.task, baselinePicks: ["failed"] })),
618
+ }, `${t.taskName}(${t.total})`)),
619
+ ),
620
+ // 第 3 层:做对 / 做错,各带题目数量
621
+ baselineTaskObj !== null && h("div", { className: c("chips"), style: { marginTop: "6px" } },
622
+ h("span", { className: c("hint"), style: { alignSelf: "center" } }, "题目:"),
623
+ [["failed", "做错"], ["passed", "做对"]].map(([key, text]) => {
624
+ const count = key === "failed" ? baselineTaskObj.failedCount : baselineTaskObj.passedCount;
625
+ const on = sel.baselinePicks.includes(key);
626
+ return h("button", {
627
+ key,
628
+ type: "button",
629
+ className: c("chip") + (on ? " " + c("chipOn") : ""),
630
+ disabled: count === 0,
631
+ title: key === "failed"
632
+ ? `${baselineTaskObj.referenceModel} 得 0 分的 ${count} 题(探针首选:做不出来的题才有分辨力)`
633
+ : `${baselineTaskObj.referenceModel} 判分 ≥0.5 的 ${count} 题(反向验证用)`,
634
+ onClick: () => setSel((prev) => ({
635
+ ...prev,
636
+ baselinePicks: prev.baselinePicks.includes(key)
637
+ ? prev.baselinePicks.filter((p) => p !== key)
638
+ : [...prev.baselinePicks, key],
639
+ })),
640
+ }, `${text}(${count})`);
641
+ }),
642
+ ),
643
+ baselineTaskObj !== null && h("span", { className: c("hint") },
644
+ `已选 ${baselineTaskObj.referenceModel} 的 ${baselineTaskObj.taskName}:`
645
+ + `本次只跑勾选的题(共 ${baselineSelectedCount} 题,有效题号 0–${Math.max(0, baselineSelectedCount - 1)}),`
646
+ + "上面的「分类/任务」被忽略;"
647
+ + "「题目序号范围」仍然生效(序号相对本题库,从 0 起、含首尾;超出上界不会报错,会按实际题数截断);"
648
+ + `release 固定为 ${baselineTaskObj.release}。`),
649
+ ),
547
650
  h("div", { className: c("field") },
548
651
  h("label", { className: c("label") }, "分类(可多选,不选 = 全部分类;括号内为该 release 下有效题数)"),
549
652
  h("div", { className: c("chips") },
@@ -601,7 +704,7 @@ window.__ModuleLoader__.load({
601
704
  h("div", { className: c("row") },
602
705
  h("div", { className: c("cardHead") },
603
706
  h("span", { className: c("label") }, `评测成绩(行 = 模型,列 = 任务)· ${sortedModels.length} 个模型 × ${matrix.tasks.length} 个任务`),
604
- h("span", { className: c("hint") }, "分数 = 该任务平均分 ×100;括号 (-没做或没做完的题数/用户选择的题数,即题目序号范围的题数);× = 未执行该任务;— = 无有效判分(如全部访问失败,不计入正确率);颜色 ≥75 绿 / 40–75 橙 / <40 红;拖换行序(存在本机),点表头按模型名或时间排序。"),
707
+ h("span", { className: c("hint") }, "分数 = 该任务平均分 ×100;括号 (-没做或没做完的题数/用户选择的题数,即题目序号范围的题数);× = 未执行该任务;— = 无有效判分(如全部访问失败,不计入正确率);颜色 ≥75 绿 / 40–75 橙 / <40 红;默认按 time 倒序(最新在前),点表头可在模型名/时间间切换排序,拖 则回到本机保存的自定义行序。"),
605
708
  ),
606
709
  h("button", {
607
710
  className: c("btn") + " " + c("btnDanger"),
@@ -692,12 +795,20 @@ window.__ModuleLoader__.load({
692
795
  h("div", { className: c("dim") }, "×"));
693
796
  }
694
797
  const sub = `(-${row.notDone ?? 0}/${row.configured ?? 0})`;
695
- const allFailed = row.answered > 0 && row.errors >= row.answered;
696
- const title = allFailed
697
- ? `本次选择 ${row.configured ?? 0} 题,其中 ${row.errors} 题全部访问失败(未做完),不计入正确率 ${sub}`
698
- : `本次选择 ${row.configured ?? 0} 题,做完 ${row.done ?? 0} 题,没做或没做完 ${row.notDone ?? 0} 题 ${sub}` +
699
- (row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "");
700
- if (allFailed || row.judged === 0) {
798
+ const emptyCount = row.empty ?? 0;
799
+ // 「没做出来」= API 失败 + 空答案(思考吃满 max-tokens 没产出正文),
800
+ // 两者都不进正确率分母
801
+ const notProduced = row.notProduced ?? row.errors ?? 0;
802
+ const allFailed = row.answered > 0 && notProduced >= row.answered;
803
+ const basePrefix = row.baselineLabel ? `【${row.baselineLabel}】` : "";
804
+ const reasons = [
805
+ row.errors > 0 ? `${row.errors} 题 API 失败` : null,
806
+ emptyCount > 0 ? `${emptyCount} 题思考占满 max-tokens、未产出答案` : null,
807
+ ].filter(Boolean).join(",");
808
+ const title = basePrefix + (allFailed
809
+ ? `本次选择 ${row.configured ?? 0} 题,全部没做出来(${reasons}),不计入正确率 ${sub}`
810
+ : `本次选择 ${row.configured ?? 0} 题,做出来 ${row.done ?? 0} 题,没做出来 ${row.notDone ?? 0} 题 ${sub}`
811
+ + (reasons ? `(其中 ${reasons})` : "")); if (allFailed || row.judged === 0) {
701
812
  return h("td", { key: taskKeyCol, className: cls, title },
702
813
  h("div", { className: c("dim") }, "—"),
703
814
  h("div", { className: c("cellSub") }, sub));
package/lib/index.js CHANGED
@@ -102,9 +102,64 @@ const RELEASES = [
102
102
  ];
103
103
  /** Sane cap so a runaway run cannot eat memory with its log. */
104
104
  const LOG_MAX_LINES = 600;
105
- /** One evaluation at a time per model, but several models may run concurrently. */
105
+ /** One evaluation at a time per model, but several models may run together. */
106
106
  const MAX_CONCURRENT_RUNS = 9;
107
107
 
108
+ /**
109
+ * 「Baseline」题库:记录某个**参考模型**在各任务上的实测对错,用来给别的模型一把同样的尺子。
110
+ *
111
+ * 结构是三层可选:参考模型(baseline)→ 任务(task)→ 对/错(passed / failed)。
112
+ * 作为"探针"用时通常只跑 failed —— 参考模型做不出来的题,才对更强的模型有分辨力;
113
+ * passed 一并留存,便于反向验证(例如确认某模型确实强于参考模型)。
114
+ * 目前只有 olympiad 做过全量实测,后续任务按同样方式补进 tasks 即可。
115
+ *
116
+ * 实现要点:题目 id 取自 LiveBench 的 question.jsonl。面板用 `--question-id` 直接指定,
117
+ * 因此不要求这些题在题库里连续;`--question-begin/--question-end` 仍然会作用在
118
+ * **id 过滤之后**的列表上,所以「题目序号范围」对 baseline 一样有效。
119
+ */
120
+ const BASELINE_SETS = {
121
+ glm53flash: {
122
+ id: "glm53flash",
123
+ label: "Baseline(glm-5.3-flash@max)",
124
+ referenceModel: "zai-coding-cn__glm-5.3-flash@max",
125
+ // 参考模型在每个任务上的实测结果:passed = 判分 >= 0.5 的题,failed = 判分 0 的题。
126
+ // 作为"探针"用时通常只跑 failed —— 参考模型做不出来的题,才对更强的模型有分辨力;
127
+ // passed 也一并留存,便于反向验证(例如确认某模型确实强于参考模型)。
128
+ // 新增任务时只在这里加一项:id 取自 LiveBench question.jsonl 的 question_id,
129
+ // 面板用 --question-id 直接指定,所以题目在题库里是否连续都无所谓。
130
+ tasks: [
131
+ {
132
+ task: "live_bench/math/olympiad",
133
+ category: "math",
134
+ taskName: "olympiad",
135
+ release: "2024-11-25",
136
+ // 2026-09-12 实测:glm-5.3-flash@max 跑满该任务全量 36 题(并发 4),
137
+ // 通过 28、失败 8;失败的 8 题全部属于"公式还原"题型。
138
+ passed: [
139
+ "ef6c6830ebf0e71d74e7ceb45a242f2b87228933eebfe47db8d798d5738cdaeb", "6e12ab2219a4051c396de5afc4228be52c2d8617c7f5621be8c364f50016c80d",
140
+ "c03217423ccb2181ef9e3106b4c116aaf1bcdbd45b3a97e04110665690177eca", "698d9bd9ad134096358dad82317dae610d9f265255aeee52c644871b83eb7a85",
141
+ "56155e5b8c1dc430cc262691aea0a75d71f7dee78b6e15304607caf0b56ecfbd", "3f55db366e970e1965bbba20e9958a4c0ea52e4224d11d875d390983485dd32b",
142
+ "3649fb05ed87c2f3ed8a933e7c8f032b54a5d6f0d23504cdd578541dd60c0f08", "e5b72d9cdb39c6e5f8798a557f119fff35d98fd165f55659e71bf6ac38b5f178",
143
+ "bc3551b5f643d8920f428909104fe2640a50f6b8a17fff6196aa78f25df28bb6", "2dd75e080f3e276f3dcf304d970e6a03973ddf777537422c9aba59559ddc9594",
144
+ "558c3c463860fcaa2b08edd9d5f0f11071944bf7d9229574862a92e6d91f1a21", "e81b96eed50d6b9b43f041d6a36ad0d9a4d00038c42c47a16efd4465d79d08a4",
145
+ "2191e02b65ac78dd0d28bc13f41786d2596dbaa560ad9d0658a27ae2814faef3", "fc903ff1f1f66a6096c9dea58c278b48f54c80a606b65f82af1730559d73755f",
146
+ "e56e4d8ed64cb6d0e43ce3c8edda0a77d769b4bda5230c1aba57442e27214694", "749116060528890530a40d111c5506f4cdec004a7c52a87ef0a4a01206c187ec",
147
+ "6d9bd0806e27f34ff88209075cad638ca0ed6662f499c74bdd3a256d3511b9fe", "9a199388f2e2bd43deac789cd248363bdcb32611480ac27cf62420b4c351759e",
148
+ "a908da1d873928f9f872dddd33c29620dfa0a54646b79bfde672bd15e6986ec9", "2c17f46f8323e8e692a1827bb18c9a9a43fcff9860b72372cb9551b7cdd532f5",
149
+ "d805a5c9f398cf5126ee84e6899cc9a3d34444a7e2337d15cdf9cc2a949649ba", "f4fdca7e355eaf9362449ebf7df78a366e239091bdb4c4a34b9dfcf9882f23de",
150
+ "aba8388e52d25b4006e78d4010c26cf8848c3e3145b0c050b9d55c8aff211fe8", "95b0921a4de64f6e510ac36b5426ecea550c4d3a6033dfbd0d93dd2213fea714",
151
+ "6df4693c156b7581ffb9a1267a84c055bc9edd8170681b9021bb837f1e9d1167", "1f6132e867a52e5c7b084011484f7fcfec9195f407c9bfb06a3f13f5b709d333",
152
+ "c6675bf6647188f84ee445590a494a8d516635bad77ace98ec61d28746839a8d", "2c5982946e20e2a85531ac00121697b263591e055ac19586db59a890fdaf98b1",
153
+ ],
154
+ failed: [
155
+ "0d3a48049dc5da31531654f59e7866832579f812b892610bd1b178fc4d010d0c", "5b9c56e9392dd999162474f10addf1a3a72f1d8b1c8bafc999ccec6f22194c18",
156
+ "11f95734f602e7d1481f9887ca7fc8bed83258e22fd5c443449ac159a4732115", "2a82215ea19fcbded36fa95df35b6e7c5f6ed28b0b5f6c3460b4223f1904a536",
157
+ "527d5f9f9cf27824b84109a495eeced7c7c097fb324b872212f6813a660e5ee4", "f22ff1f6c067d0e085eb79fe5c2e35df01f9edb2bcc73e59e1e15eefd32b8f07",
158
+ "6dfb6aade6429e2cca0718a497a442299a57199dfd547813471f4cf30ed6c7c7", "0499deda2f068008d488551abf96b4b758c6ed6b79cd2ec6a204d1250b140421",
159
+ ],
160
+ },
161
+ ], },
162
+ };
108
163
  /** Resolve the profile directory from the config-tree anchor (plugin-market pattern). */
109
164
  function resolveProfileDir(ctx) {
110
165
  if (typeof ctx.baseUrl === "string" && ctx.baseUrl.startsWith("file:")) {
@@ -299,6 +354,51 @@ function scanCategoryTasks(dataDir) {
299
354
  return tasks;
300
355
  }
301
356
 
357
+ /**
358
+ * 缓存版的 scanCategoryTasks:/config 每次都要它,/results 也要用它算"某 release 下的
359
+ * 有效题数"。question.jsonl 是静态文件,扫描结果缓存 60s 足够。
360
+ */
361
+ let taskScanCache = { at: 0, data: null };
362
+ function scanCategoryTasksCached(dataDir) {
363
+ const now = Date.now();
364
+ if (taskScanCache.data !== null && now - taskScanCache.at < 60000) return taskScanCache.data;
365
+ const data = scanCategoryTasks(dataDir);
366
+ taskScanCache = { at: now, data };
367
+ return data;
368
+ }
369
+
370
+ /**
371
+ * 某个任务在指定 release 下的**有效题数**。
372
+ * LiveBench 的 --question-begin/--question-end 作用在 release 过滤后的列表上,
373
+ * 所以边界裁剪必须用这个数,而不是 question.jsonl 的原始行数。
374
+ * @returns {number|null} null 表示没有该任务的数据(调用方回退到原始题数)。
375
+ */
376
+ function releaseQuestionCount(tasks, category, task, release) {
377
+ const meta = (tasks[category] ?? {})[task];
378
+ if (!meta || !Array.isArray(meta.buckets) || typeof release !== "string" || release.length === 0) return null;
379
+ let n = 0;
380
+ for (const b of meta.buckets) {
381
+ const releasedOk = b.r !== "" && b.r <= release;
382
+ const notRemoved = b.rm === "" || b.rm > release;
383
+ if (releasedOk && notRemoved) n += b.n;
384
+ }
385
+ return n;
386
+ }
387
+
388
+ /**
389
+ * Python `list[begin:end]` 的长度,含越界裁剪。
390
+ * 面板的「止」是闭区间,所以 end 先 +1 再按切片语义算:
391
+ * len = min(total, end+1) - min(total, begin)
392
+ * 只写 min(total, end-begin+1) 会在 begin 也越界时算多
393
+ * (8 题填 6-9,实际跑 2 题却算成 4 题)。
394
+ */
395
+ function clampedRangeLength(total, begin, end) {
396
+ if (!Number.isFinite(total) || total <= 0) return null;
397
+ const b = Math.max(0, Math.min(total, Number(begin)));
398
+ const e = Math.max(0, Math.min(total, Number(end) + 1));
399
+ return Math.max(0, e - b);
400
+ }
401
+
302
402
  function readDirSafe(dir) {
303
403
  try {
304
404
  return readdirNames(dir);
@@ -510,6 +610,23 @@ function parseAnswerLineLight(line) {
510
610
  */
511
611
  let resultsCache = { at: 0, data: null };
512
612
 
613
+ /**
614
+ * 答案行是否"没产出内容"。
615
+ *
616
+ * 两类:
617
+ * 1. `eval_status: token_exhaustion` —— 推理模型把 max_tokens 全烧在思考上,正文为空;
618
+ * 2. `turns` / `choices` 是空串或空数组 —— 上游 200 但没给内容。
619
+ *
620
+ * 这两种都不是"做错了",而是**没做出来**,不该进正确率的分母(用户口径:
621
+ * 做对的题 / 做出来的题)。它们也不是 $ERROR$(那是 API 失败),所以单列一类。
622
+ *
623
+ * 用正则直接扫原始行,避免为了判定空答案去 JSON.parse 上百万字节的答案文件。
624
+ */
625
+ const EMPTY_ANSWER_RE = /"eval_status"\s*:\s*"token_exhaustion"|"turns"\s*:\s*\[\s*(?:""\s*)?\]|"choices"\s*:\s*\[\s*""\s*\]/;
626
+ function isEmptyAnswerLine(line) {
627
+ return EMPTY_ANSWER_RE.test(line);
628
+ }
629
+
513
630
  async function readTextFileAsync(path) {
514
631
  const fsPromises = await import("node:fs/promises");
515
632
  try {
@@ -551,11 +668,12 @@ async function computeResults(dataDir) {
551
668
  const text = await readTextFileAsync(join(answerDir, file));
552
669
  if (text === null) continue;
553
670
  const key = `${model}\u0000${task}`;
554
- const stat = answers.get(key) ?? { answered: 0, errors: 0 };
671
+ const stat = answers.get(key) ?? { answered: 0, errors: 0, empty: 0 };
555
672
  for (const line of text.split("\n")) {
556
673
  if (line.trim().length === 0) continue;
557
674
  stat.answered += 1;
558
675
  if (line.includes('"$ERROR$"')) stat.errors += 1;
676
+ else if (isEmptyAnswerLine(line)) stat.empty += 1;
559
677
  }
560
678
  // 空文件(尚未产出答案,或成绩已被删除只留下 0 字节壳)不计入结果:
561
679
  // 否则删掉的记录会在下一次扫描时凭“文件名仍在”重新冒出来。
@@ -591,8 +709,13 @@ async function computeResults(dataDir) {
591
709
  const category = entry?.category ?? catByTask.get(task) ?? "";
592
710
  const total = totals.get(task) ?? 0;
593
711
  const judgedCount = entry?.n ?? 0;
594
- // 做完的题 = 实际产出的有效答案(尝试数 - 访问失败数)
595
- const done = Math.max(0, stat.answered - stat.errors);
712
+ // 「做出来」的题 = 尝试数 - API 失败 - 空答案。
713
+ // 关键:思考吃满 max_tokens、正文为空的题属于"没做出来",不是"做错了",
714
+ // 必须从正确率分母里剔除,否则一个模型 8 题里只答出 1 题(0.9167 分)
715
+ // 会被算成 11.5% 而不是 91.7%。
716
+ const empty = stat.empty ?? 0;
717
+ const notProduced = stat.errors + empty;
718
+ const done = Math.max(0, stat.answered - notProduced);
596
719
  // 选择题数 = 用户在「题目序号范围」里选择的题数(end 含端点)。
597
720
  // 有元数据就直接用范围大小;没有元数据(历史数据)时用本次实际写入的答案行数兜底:
598
721
  // LiveBench 会为范围内每一题写一行(访问失败也写 $ERROR$),所以跑完后
@@ -608,15 +731,27 @@ async function computeResults(dataDir) {
608
731
  return true;
609
732
  });
610
733
  if (inScope) {
734
+ // 「可选题目总数」的基准,按优先级取:
735
+ // baseline 子集大小 > 该任务在本次 release 下的有效题数 > question.jsonl 原始行数。
736
+ // 基准取错会把越界的范围当成有效选择(题库只有 8 题而用户填 0-8 时算出 9 题),
737
+ // 于是 notDone 永远多 1,全做对也显示 (-1/8)。
738
+ const byRelease = releaseQuestionCount(scanCategoryTasksCached(dataDir), category, task, meta.release);
739
+ const baselineTotal = Number.isFinite(Number(meta.baselineSize)) && Number(meta.baselineSize) > 0
740
+ ? Number(meta.baselineSize)
741
+ : null;
742
+ const scopeTotal = baselineTotal ?? byRelease ?? total;
611
743
  const hasRange = meta.begin !== null && meta.begin !== undefined && meta.end !== null && meta.end !== undefined;
612
- if (hasRange) configured = Math.min(total, Number(meta.end) - Number(meta.begin) + 1);
744
+ if (hasRange) {
745
+ const len = clampedRangeLength(scopeTotal, Number(meta.begin), Number(meta.end));
746
+ if (len !== null) configured = len;
747
+ }
613
748
  }
614
749
  }
615
750
  // 实际尝试数比设定还多(断点重跑等)时,以实际为准
616
751
  if (configured < stat.answered) configured = stat.answered;
617
752
  const notDone = Math.max(0, configured - done);
618
- // 正确率 = 做对的题 / 已判分的做完题(访问失败/未做的题不计入分母)
619
- const judgedDone = Math.max(0, judgedCount - stat.errors);
753
+ // 正确率 = 做对的题 / **做出来的题**(API 失败、思考吃满 token 没产出答案的都不进分母)
754
+ const judgedDone = Math.max(0, judgedCount - notProduced);
620
755
  const score = entry && judgedDone > 0 ? (entry.sum / judgedDone) * 100 : null;
621
756
  return {
622
757
  model,
@@ -628,11 +763,15 @@ async function computeResults(dataDir) {
628
763
  time: entry && entry.time > 0 ? entry.time : null,
629
764
  answered: stat.answered,
630
765
  errors: stat.errors,
766
+ empty,
767
+ notProduced,
631
768
  done,
632
769
  configured,
633
770
  notDone,
634
771
  // 本次运行的开始时间(ISO);旧数据没有元数据时为 null,前端退回解析 displayName 里的运行戳
635
772
  runStartedAt: meta !== undefined && typeof meta.startedAt === "string" ? meta.startedAt : null,
773
+ // 非 null 表示这一行跑的是 Baseline 题库子集,不是该任务全量
774
+ baselineLabel: meta !== undefined && typeof meta.baselineLabel === "string" ? meta.baselineLabel : null,
636
775
  };
637
776
  });
638
777
  // taskTotals 同时给出裸任务名与 `category/task` 两种键,便于前端按列取题目总数
@@ -743,7 +882,7 @@ function apply(ctx) {
743
882
  }
744
883
  const providerId = typeof body.provider === "string" ? body.provider : "";
745
884
  const modelId = typeof body.model === "string" ? body.model.trim() : "";
746
- const release = typeof body.release === "string" ? body.release : "2024-11-25";
885
+ let release = typeof body.release === "string" ? body.release : "2024-11-25";
747
886
  const category = typeof body.category === "string" && body.category.length > 0 ? body.category : null;
748
887
  const task = typeof body.task === "string" && body.task.length > 0 ? body.task : null;
749
888
  const reasoningEffort = typeof body.reasoningEffort === "string" && body.reasoningEffort.length > 0 && body.reasoningEffort !== "default" ? body.reasoningEffort : null;
@@ -764,8 +903,44 @@ function apply(ctx) {
764
903
  // Multi-select support: the client resolves its category/task chips into
765
904
  // an explicit bench-path list ("live_bench", "live_bench/<cat>",
766
905
  // "live_bench/<cat>/<task>"); gen_api_answer accepts nargs="+".
906
+ // Baseline 题库则完全绕开分类/任务选择("不选 = 全部分类"不适用),
907
+ // 锁定到它自己的任务 + 一组 question id。
908
+ const baselineKey = typeof body.baseline === "string" && body.baseline.length > 0 ? body.baseline : null;
909
+ const baseline = baselineKey !== null ? (BASELINE_SETS[baselineKey] ?? null) : null;
910
+ if (baselineKey !== null && baseline === null) {
911
+ return { status: 400, payload: { ok: false, error: `unknown baseline: ${baselineKey}` } };
912
+ }
767
913
  let benchNames = null;
768
- if (Array.isArray(body.benchNames) && body.benchNames.length > 0) {
914
+ // Baseline 是三层选择:参考模型 + 任务 + 对/错。服务端负责把选择解析成 question id 列表,
915
+ // 客户端只送选择,不送 id —— 免得界面与题库数据脱节。
916
+ let baselinePick = null;
917
+ if (baseline !== null) {
918
+ const taskKey = typeof body.baselineTask === "string" ? body.baselineTask : "";
919
+ const picks = Array.isArray(body.baselinePicks)
920
+ ? body.baselinePicks.filter((p) => p === "passed" || p === "failed")
921
+ : [];
922
+ const baselineTask = baseline.tasks.find((t) => t.task === taskKey) ?? null;
923
+ if (baselineTask === null) {
924
+ return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}: 未选择任务(或任务不存在):${taskKey}` } };
925
+ }
926
+ if (picks.length === 0) {
927
+ return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}/${taskKey}: 请至少选择「做对」或「做错」之一` } };
928
+ }
929
+ const ids = [];
930
+ const seen = new Set();
931
+ for (const pick of picks) {
932
+ for (const qid of baselineTask[pick]) {
933
+ if (!seen.has(qid)) { seen.add(qid); ids.push(qid); }
934
+ }
935
+ }
936
+ if (ids.length === 0) {
937
+ return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}/${taskKey}: 所选分组没有题目` } };
938
+ }
939
+ // baseline 的 question id 属于特定 release,跟着用户选别的 release 会一题都匹配不到
940
+ release = baselineTask.release;
941
+ benchNames = [baselineTask.task];
942
+ baselinePick = { label: baseline.label, task: baselineTask.task, picks, ids };
943
+ } else if (Array.isArray(body.benchNames) && body.benchNames.length > 0) {
769
944
  if (body.benchNames.length > 24) {
770
945
  return { status: 400, payload: { ok: false, error: "too many bench names (max 24)" } };
771
946
  }
@@ -824,13 +999,23 @@ function apply(ctx) {
824
999
  "--bench-name",
825
1000
  ...(benchNames !== null ? benchNames : [benchParts.join("/")]),
826
1001
  "--livebench-release-option", release,
827
- "--max-tokens", String(asInt(body.maxTokens, 256, 32768, 32000)),
1002
+ // 上限放到 200k:推理模型在难题上会把 max_tokens 全烧在思考里,正文为空
1003
+ // (eval_status=token_exhaustion),此时"做不出来"其实是预算不够而不是能力不够。
1004
+ // 旧上限 32768 在 olympiad 这类题上会直接把参考模型卡死。
1005
+ "--max-tokens", String(asInt(body.maxTokens, 256, 200000, 32000)),
828
1006
  "--parallel-requests", String(asInt(body.parallel, 1, 8, 1)),
829
1007
  // 流式:中转网关(Cloudflare 等)对非流式请求有 ~100s 超时(524),
830
1008
  // 高推理强度模型思考数分钟必然超时。流式保持字节流动可规避。
831
1009
  "--stream",
832
1010
  "--mode", "single",
833
1011
  ];
1012
+ // Baseline:直接点名 question id。--question-id 是 nargs="+",后面跟一串 id;
1013
+ // 它和后面的 --question-begin/--question-end 叠加时,范围作用在**id 过滤后**的
1014
+ // 列表上(gen_api_answer: load_questions(..., question_id) 之后才做 [begin:end]),
1015
+ // 所以「题目序号范围」对 baseline 题库同样有效。
1016
+ if (baselinePick !== null) {
1017
+ args.push("--question-id", ...baselinePick.ids);
1018
+ }
834
1019
  if (body.begin !== undefined && body.begin !== null && `${body.begin}`.length > 0) {
835
1020
  args.push("--question-begin", String(asInt(body.begin, 0, 100000, 0)));
836
1021
  }
@@ -897,6 +1082,17 @@ function apply(ctx) {
897
1082
  benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
898
1083
  begin: body.begin ?? null,
899
1084
  end: body.end ?? null,
1085
+ // 算"本次设定题数"要用 release 过滤后的有效题数来裁剪边界,所以必须记下来
1086
+ release,
1087
+ // baseline 评测标出来:成绩表里同一列会混着"全量"和"题库子集"两种分数,
1088
+ // 不标注就没法解读。
1089
+ baseline: baselinePick !== null ? baselineKey : null,
1090
+ baselineLabel: baselinePick !== null
1091
+ ? `${baselinePick.label} · ${baselinePick.task.split("/").slice(-1)[0]} · ${baselinePick.picks.map((p) => (p === "passed" ? "做对" : "做错")).join("+")}`
1092
+ : null,
1093
+ baselineTask: baselinePick !== null ? baselinePick.task : null,
1094
+ baselinePicks: baselinePick !== null ? baselinePick.picks : null,
1095
+ baselineSize: baselinePick !== null ? baselinePick.ids.length : null,
900
1096
  startedAt: new Date().toISOString(),
901
1097
  }), "utf8");
902
1098
  } catch { /* 元数据写入失败不影响评测 */ }
@@ -1005,7 +1201,22 @@ function apply(ctx) {
1005
1201
  available: layout.available,
1006
1202
  root: layout.root,
1007
1203
  releases: RELEASES,
1008
- tasks: scanCategoryTasks(layout.dataDir),
1204
+ tasks: scanCategoryTasksCached(layout.dataDir),
1205
+ // Baseline 题库:参考模型在各任务上的实测对错,供"探针"式快速评测选用
1206
+ baselines: Object.values(BASELINE_SETS).map((b) => ({
1207
+ id: b.id,
1208
+ label: b.label,
1209
+ referenceModel: b.referenceModel,
1210
+ tasks: b.tasks.map((t) => ({
1211
+ task: t.task,
1212
+ category: t.category,
1213
+ taskName: t.taskName,
1214
+ release: t.release,
1215
+ total: t.passed.length + t.failed.length,
1216
+ passedCount: t.passed.length,
1217
+ failedCount: t.failed.length,
1218
+ })),
1219
+ })),
1009
1220
  providers: providers.map(({ id, name: pname, models, api, baseURL }) => ({
1010
1221
  id,
1011
1222
  name: pname,
@@ -1223,4 +1434,4 @@ function apply(ctx) {
1223
1434
  }), `${name}: delete route`);
1224
1435
  }
1225
1436
 
1226
- export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName };
1437
+ export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount, isEmptyAnswerLine };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.2.18",
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.",
3
+ "version": "0.2.21",
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)",
7
7
  "type": "module",