dsh-livebench-panel 0.2.17 → 0.2.20
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 +33 -1
- package/lib/client.js +125 -22
- package/lib/index.js +203 -19
- package/package.json +2 -2
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
|
-
| 题目序号范围 |
|
|
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(
|
|
135
|
-
const [sortDir, setSortDir] = useState("
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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"),
|
|
@@ -693,11 +796,11 @@ window.__ModuleLoader__.load({
|
|
|
693
796
|
}
|
|
694
797
|
const sub = `(-${row.notDone ?? 0}/${row.configured ?? 0})`;
|
|
695
798
|
const allFailed = row.answered > 0 && row.errors >= row.answered;
|
|
696
|
-
const
|
|
799
|
+
const basePrefix = row.baselineLabel ? `【${row.baselineLabel}】` : "";
|
|
800
|
+
const title = basePrefix + (allFailed
|
|
697
801
|
? `本次选择 ${row.configured ?? 0} 题,其中 ${row.errors} 题全部访问失败(未做完),不计入正确率 ${sub}`
|
|
698
802
|
: `本次选择 ${row.configured ?? 0} 题,做完 ${row.done ?? 0} 题,没做或没做完 ${row.notDone ?? 0} 题 ${sub}` +
|
|
699
|
-
(row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "");
|
|
700
|
-
if (allFailed || row.judged === 0) {
|
|
803
|
+
(row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "")); if (allFailed || row.judged === 0) {
|
|
701
804
|
return h("td", { key: taskKeyCol, className: cls, title },
|
|
702
805
|
h("div", { className: c("dim") }, "—"),
|
|
703
806
|
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
|
|
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);
|
|
@@ -608,8 +708,20 @@ async function computeResults(dataDir) {
|
|
|
608
708
|
return true;
|
|
609
709
|
});
|
|
610
710
|
if (inScope) {
|
|
711
|
+
// 「可选题目总数」的基准,按优先级取:
|
|
712
|
+
// baseline 子集大小 > 该任务在本次 release 下的有效题数 > question.jsonl 原始行数。
|
|
713
|
+
// 基准取错会把越界的范围当成有效选择(题库只有 8 题而用户填 0-8 时算出 9 题),
|
|
714
|
+
// 于是 notDone 永远多 1,全做对也显示 (-1/8)。
|
|
715
|
+
const byRelease = releaseQuestionCount(scanCategoryTasksCached(dataDir), category, task, meta.release);
|
|
716
|
+
const baselineTotal = Number.isFinite(Number(meta.baselineSize)) && Number(meta.baselineSize) > 0
|
|
717
|
+
? Number(meta.baselineSize)
|
|
718
|
+
: null;
|
|
719
|
+
const scopeTotal = baselineTotal ?? byRelease ?? total;
|
|
611
720
|
const hasRange = meta.begin !== null && meta.begin !== undefined && meta.end !== null && meta.end !== undefined;
|
|
612
|
-
if (hasRange)
|
|
721
|
+
if (hasRange) {
|
|
722
|
+
const len = clampedRangeLength(scopeTotal, Number(meta.begin), Number(meta.end));
|
|
723
|
+
if (len !== null) configured = len;
|
|
724
|
+
}
|
|
613
725
|
}
|
|
614
726
|
}
|
|
615
727
|
// 实际尝试数比设定还多(断点重跑等)时,以实际为准
|
|
@@ -633,6 +745,8 @@ async function computeResults(dataDir) {
|
|
|
633
745
|
notDone,
|
|
634
746
|
// 本次运行的开始时间(ISO);旧数据没有元数据时为 null,前端退回解析 displayName 里的运行戳
|
|
635
747
|
runStartedAt: meta !== undefined && typeof meta.startedAt === "string" ? meta.startedAt : null,
|
|
748
|
+
// 非 null 表示这一行跑的是 Baseline 题库子集,不是该任务全量
|
|
749
|
+
baselineLabel: meta !== undefined && typeof meta.baselineLabel === "string" ? meta.baselineLabel : null,
|
|
636
750
|
};
|
|
637
751
|
});
|
|
638
752
|
// taskTotals 同时给出裸任务名与 `category/task` 两种键,便于前端按列取题目总数
|
|
@@ -743,7 +857,7 @@ function apply(ctx) {
|
|
|
743
857
|
}
|
|
744
858
|
const providerId = typeof body.provider === "string" ? body.provider : "";
|
|
745
859
|
const modelId = typeof body.model === "string" ? body.model.trim() : "";
|
|
746
|
-
|
|
860
|
+
let release = typeof body.release === "string" ? body.release : "2024-11-25";
|
|
747
861
|
const category = typeof body.category === "string" && body.category.length > 0 ? body.category : null;
|
|
748
862
|
const task = typeof body.task === "string" && body.task.length > 0 ? body.task : null;
|
|
749
863
|
const reasoningEffort = typeof body.reasoningEffort === "string" && body.reasoningEffort.length > 0 && body.reasoningEffort !== "default" ? body.reasoningEffort : null;
|
|
@@ -764,8 +878,44 @@ function apply(ctx) {
|
|
|
764
878
|
// Multi-select support: the client resolves its category/task chips into
|
|
765
879
|
// an explicit bench-path list ("live_bench", "live_bench/<cat>",
|
|
766
880
|
// "live_bench/<cat>/<task>"); gen_api_answer accepts nargs="+".
|
|
881
|
+
// Baseline 题库则完全绕开分类/任务选择("不选 = 全部分类"不适用),
|
|
882
|
+
// 锁定到它自己的任务 + 一组 question id。
|
|
883
|
+
const baselineKey = typeof body.baseline === "string" && body.baseline.length > 0 ? body.baseline : null;
|
|
884
|
+
const baseline = baselineKey !== null ? (BASELINE_SETS[baselineKey] ?? null) : null;
|
|
885
|
+
if (baselineKey !== null && baseline === null) {
|
|
886
|
+
return { status: 400, payload: { ok: false, error: `unknown baseline: ${baselineKey}` } };
|
|
887
|
+
}
|
|
767
888
|
let benchNames = null;
|
|
768
|
-
|
|
889
|
+
// Baseline 是三层选择:参考模型 + 任务 + 对/错。服务端负责把选择解析成 question id 列表,
|
|
890
|
+
// 客户端只送选择,不送 id —— 免得界面与题库数据脱节。
|
|
891
|
+
let baselinePick = null;
|
|
892
|
+
if (baseline !== null) {
|
|
893
|
+
const taskKey = typeof body.baselineTask === "string" ? body.baselineTask : "";
|
|
894
|
+
const picks = Array.isArray(body.baselinePicks)
|
|
895
|
+
? body.baselinePicks.filter((p) => p === "passed" || p === "failed")
|
|
896
|
+
: [];
|
|
897
|
+
const baselineTask = baseline.tasks.find((t) => t.task === taskKey) ?? null;
|
|
898
|
+
if (baselineTask === null) {
|
|
899
|
+
return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}: 未选择任务(或任务不存在):${taskKey}` } };
|
|
900
|
+
}
|
|
901
|
+
if (picks.length === 0) {
|
|
902
|
+
return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}/${taskKey}: 请至少选择「做对」或「做错」之一` } };
|
|
903
|
+
}
|
|
904
|
+
const ids = [];
|
|
905
|
+
const seen = new Set();
|
|
906
|
+
for (const pick of picks) {
|
|
907
|
+
for (const qid of baselineTask[pick]) {
|
|
908
|
+
if (!seen.has(qid)) { seen.add(qid); ids.push(qid); }
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (ids.length === 0) {
|
|
912
|
+
return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}/${taskKey}: 所选分组没有题目` } };
|
|
913
|
+
}
|
|
914
|
+
// baseline 的 question id 属于特定 release,跟着用户选别的 release 会一题都匹配不到
|
|
915
|
+
release = baselineTask.release;
|
|
916
|
+
benchNames = [baselineTask.task];
|
|
917
|
+
baselinePick = { label: baseline.label, task: baselineTask.task, picks, ids };
|
|
918
|
+
} else if (Array.isArray(body.benchNames) && body.benchNames.length > 0) {
|
|
769
919
|
if (body.benchNames.length > 24) {
|
|
770
920
|
return { status: 400, payload: { ok: false, error: "too many bench names (max 24)" } };
|
|
771
921
|
}
|
|
@@ -791,7 +941,6 @@ function apply(ctx) {
|
|
|
791
941
|
const _pad = (n) => String(n).padStart(2, "0");
|
|
792
942
|
const runStamp = `r${_now.getFullYear()}${_pad(_now.getMonth() + 1)}${_pad(_now.getDate())}-${_pad(_now.getHours())}${_pad(_now.getMinutes())}${_pad(_now.getSeconds())}`;
|
|
793
943
|
const displayName = displayModelName(providerId || "direct", modelId) + (hasEffortSuffix ? "@" + effort : "") + "__" + runStamp;
|
|
794
|
-
let cliModel = modelId;
|
|
795
944
|
let writeError = null; // Anthropic-protocol proxies cannot go through --api-base (that path
|
|
796
945
|
// speaks OpenAI Chat Completions). Instead the generated model config
|
|
797
946
|
// selects LiveBench's native anthropic client and the spawn env points
|
|
@@ -803,22 +952,24 @@ function apply(ctx) {
|
|
|
803
952
|
const isOpenAIResponsesRoute = provider
|
|
804
953
|
&& (provider.api === "openai-responses" || provider.api === "openai_responses")
|
|
805
954
|
&& provider.baseURL;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
955
|
+
// 每次都写自己的模型配置,并且 --model 永远用 display-name。
|
|
956
|
+
// 不能图省事把裸模型名交给 LiveBench:它会用 get_model_config(裸名) 去撞它自带的
|
|
957
|
+
// 模型库(例如 "kimi-k3" 命中 moonshotai.yml),命中后就改用那份配置的
|
|
958
|
+
// provider / api_kwargs / max_tokens —— 实测会把 temperature 悄悄变成 1.0、
|
|
959
|
+
// max_tokens 变成 131072,等于用户选的 provider 被换掉。写了自己的配置,
|
|
960
|
+
// 解析结果就是确定的 {local: <modelId>} + --api-base。
|
|
961
|
+
writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), {
|
|
962
|
+
displayName,
|
|
963
|
+
modelId,
|
|
964
|
+
reasoningEffort: isAnthropicRoute ? null : effort,
|
|
965
|
+
protocol: isAnthropicRoute ? "anthropic" : isOpenAIResponsesRoute ? "openai_responses" : "openai",
|
|
966
|
+
});
|
|
967
|
+
if (writeError) return { status: 500, payload: { ok: false, error: writeError } };
|
|
817
968
|
|
|
818
969
|
const benchParts = ["live_bench", ...(category ? [category] : []), ...(task ? [task] : [])];
|
|
819
970
|
const args = [
|
|
820
971
|
"run_livebench.py",
|
|
821
|
-
"--model",
|
|
972
|
+
"--model", displayName,
|
|
822
973
|
"--model-display-name", displayName,
|
|
823
974
|
"--bench-name",
|
|
824
975
|
...(benchNames !== null ? benchNames : [benchParts.join("/")]),
|
|
@@ -830,6 +981,13 @@ function apply(ctx) {
|
|
|
830
981
|
"--stream",
|
|
831
982
|
"--mode", "single",
|
|
832
983
|
];
|
|
984
|
+
// Baseline:直接点名 question id。--question-id 是 nargs="+",后面跟一串 id;
|
|
985
|
+
// 它和后面的 --question-begin/--question-end 叠加时,范围作用在**id 过滤后**的
|
|
986
|
+
// 列表上(gen_api_answer: load_questions(..., question_id) 之后才做 [begin:end]),
|
|
987
|
+
// 所以「题目序号范围」对 baseline 题库同样有效。
|
|
988
|
+
if (baselinePick !== null) {
|
|
989
|
+
args.push("--question-id", ...baselinePick.ids);
|
|
990
|
+
}
|
|
833
991
|
if (body.begin !== undefined && body.begin !== null && `${body.begin}`.length > 0) {
|
|
834
992
|
args.push("--question-begin", String(asInt(body.begin, 0, 100000, 0)));
|
|
835
993
|
}
|
|
@@ -896,6 +1054,17 @@ function apply(ctx) {
|
|
|
896
1054
|
benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
|
|
897
1055
|
begin: body.begin ?? null,
|
|
898
1056
|
end: body.end ?? null,
|
|
1057
|
+
// 算"本次设定题数"要用 release 过滤后的有效题数来裁剪边界,所以必须记下来
|
|
1058
|
+
release,
|
|
1059
|
+
// baseline 评测标出来:成绩表里同一列会混着"全量"和"题库子集"两种分数,
|
|
1060
|
+
// 不标注就没法解读。
|
|
1061
|
+
baseline: baselinePick !== null ? baselineKey : null,
|
|
1062
|
+
baselineLabel: baselinePick !== null
|
|
1063
|
+
? `${baselinePick.label} · ${baselinePick.task.split("/").slice(-1)[0]} · ${baselinePick.picks.map((p) => (p === "passed" ? "做对" : "做错")).join("+")}`
|
|
1064
|
+
: null,
|
|
1065
|
+
baselineTask: baselinePick !== null ? baselinePick.task : null,
|
|
1066
|
+
baselinePicks: baselinePick !== null ? baselinePick.picks : null,
|
|
1067
|
+
baselineSize: baselinePick !== null ? baselinePick.ids.length : null,
|
|
899
1068
|
startedAt: new Date().toISOString(),
|
|
900
1069
|
}), "utf8");
|
|
901
1070
|
} catch { /* 元数据写入失败不影响评测 */ }
|
|
@@ -1004,7 +1173,22 @@ function apply(ctx) {
|
|
|
1004
1173
|
available: layout.available,
|
|
1005
1174
|
root: layout.root,
|
|
1006
1175
|
releases: RELEASES,
|
|
1007
|
-
tasks:
|
|
1176
|
+
tasks: scanCategoryTasksCached(layout.dataDir),
|
|
1177
|
+
// Baseline 题库:参考模型在各任务上的实测对错,供"探针"式快速评测选用
|
|
1178
|
+
baselines: Object.values(BASELINE_SETS).map((b) => ({
|
|
1179
|
+
id: b.id,
|
|
1180
|
+
label: b.label,
|
|
1181
|
+
referenceModel: b.referenceModel,
|
|
1182
|
+
tasks: b.tasks.map((t) => ({
|
|
1183
|
+
task: t.task,
|
|
1184
|
+
category: t.category,
|
|
1185
|
+
taskName: t.taskName,
|
|
1186
|
+
release: t.release,
|
|
1187
|
+
total: t.passed.length + t.failed.length,
|
|
1188
|
+
passedCount: t.passed.length,
|
|
1189
|
+
failedCount: t.failed.length,
|
|
1190
|
+
})),
|
|
1191
|
+
})),
|
|
1008
1192
|
providers: providers.map(({ id, name: pname, models, api, baseURL }) => ({
|
|
1009
1193
|
id,
|
|
1010
1194
|
name: pname,
|
|
@@ -1222,4 +1406,4 @@ function apply(ctx) {
|
|
|
1222
1406
|
}), `${name}: delete route`);
|
|
1223
1407
|
}
|
|
1224
1408
|
|
|
1225
|
-
export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName };
|
|
1409
|
+
export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-livebench-panel",
|
|
3
|
-
"version": "0.2.
|
|
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.20",
|
|
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",
|