dsh-livebench-panel 0.1.1

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 ADDED
@@ -0,0 +1,78 @@
1
+ # dsh-livebench-panel
2
+
3
+ DSH web 插件:在 **Trajectory 视图(轨迹视图)** 的「对话 / 轨迹」标签右侧新增一个 **LiveBench** 标签页。点进去即可用下拉框选择参数,直接对本机 LiveBench 发起评测并查看成绩。
4
+
5
+ ## ⚠️ 依赖要求(必读)
6
+
7
+ 本插件只是**控制台**,真正的评测由 [LiveBench](https://github.com/LiveBench/LiveBench) 项目在本机完成。仅安装插件时,面板底部会显示同样的安装指引。完整安装步骤(Windows,约 10–20 分钟):
8
+
9
+ ```bat
10
+ :: 1) 克隆 LiveBench(放到默认路径可免配置;装在其它目录请设环境变量 DSH_LIVEBENCH_HOME 指向它)
11
+ git clone https://github.com/LiveBench/LiveBench V:\PythonProject\C_UtilizeSpace\LiveBench
12
+
13
+ :: 2) 用 Python 3.11 建虚拟环境并安装(3.10 会因 litellm 报 NotRequired 错误)
14
+ cd /d V:\PythonProject\C_UtilizeSpace\LiveBench
15
+ py -3.11 -m venv .venv
16
+ .venv\Scripts\python -m pip install -e .
17
+
18
+ :: 3) 仅评测 coding 类需要:安装评分依赖(较大,含 TensorFlow)
19
+ .venv\Scripts\python -m pip install -r livebench\code_runner\requirements_eval.txt
20
+
21
+ :: 4) 下载题目数据(约 250MB)
22
+ cd livebench
23
+ ..\.venv\Scripts\python download_questions.py
24
+
25
+ :: 5) 重启 dsh web,回到面板点「刷新」
26
+ ```
27
+
28
+ 装在其它目录时:设置**系统环境变量** `DSH_LIVEBENCH_HOME=<你的LiveBench目录>`,重启 dsh web 生效。
29
+
30
+ ## 面板选项说明
31
+
32
+ | 选项 | 含义 | 选择建议 |
33
+ |---|---|---|
34
+ | 模型 | harness 全部 provider 的全部模型(含内置 deepseek-official);分组展示 | 首次验证选小任务 + 便宜模型 |
35
+ | 推理强度 | 该模型的 reasoning effort(无配置则禁用);编码进条目名,不同强度独立出分 | 日常 `medium`/`high`;对比测试固定同一档 |
36
+ | 题集 release | LiveBench 题目发布批次 | 公开题目最全的是 `2024-11-25`(推荐) |
37
+ | 分类 | 六大类:coding / math / reasoning / language / data_analysis / instruction_following | 首次验证选 `language` |
38
+ | 任务 | 分类下的具体任务(如 language/typos 拼写纠错) | 首次验证选 `typos`(短平快) |
39
+ | 题目序号范围 | 该任务题目的起止下标(0 起) | 冒烟测试填 0–1(只跑 2 题) |
40
+ | max-tokens | 单次回答的 token 上限 | 推理模型给 8192+,否则思考被截断判 0 分 |
41
+
42
+ ## 功能
43
+
44
+ - **标签页位置**:`conversation.view` 槽位 `id: "livebench"`、`order: 20`(对话 = 0,轨迹 = 10,LiveBench 排最右)。
45
+ - **模型下拉框**:读取 `$DSH_HOME/settings.yaml` 的 `llm-pi-ai.providers` **并合并内置 `deepseek-official` 路由**(`@deepseek-ai/dsh-llm-deepseek` 注册的 DeepSeek-V4-Flash / V4-Pro / V4-Flash-Vision-Exp,默认 `https://api.deepseek.com` + `DEEPSEEK_API_KEY`,用户 `llm-deepseek` 设置节可覆盖),与 harness 模型选择同源,展示**全部 provider / 全部模型**;`openai-completions` 且配置了 `baseURL` 的 provider 会被标记为可直连 LiveBench(`--api-base` 路由)。
46
+ - **推理强度下拉框**(模型右侧):来自模型配置的 `reasoningEfforts` 映射(如 gpt-5.6 系 off/low/medium/high/xhigh/max,DeepSeek off/low/high/max)。选定后:
47
+ - 插件向 `livebench/model/model_configs/dsh_panel_generated.yaml` 写入一条模型配置,经 LiveBench 的 `api_kwargs.default.reasoning_effort` 透传给 API(`off` 表示不透传、由后端走默认);
48
+ - 强度编码进 display-name(如 `code-gpt__gpt-5.6-sol@high`),**不同强度在成绩表中是独立条目**,可直接对比。
49
+ - **参数下拉框**:题集 release(LiveBench 全部 releases)、分类(coding/math/reasoning/language/data_analysis/instruction_following)、任务(随分类联动)、题目序号范围、max-tokens。
50
+ - **运行控制**:开始 / 停止 / 刷新;同一时间只允许一个评测;实时滚动日志(每 2.5s 轮询)。
51
+ - **成绩表**:直接读取 `data/live_bench/**/model_judgment/ground_truth_judgment.jsonl` 计算 模型 × 任务 平均分(分数 = 正确率 ×100,附已判/总题数),无需等 LiveBench 自己出榜。
52
+
53
+ ## 依赖
54
+
55
+ - 本机 LiveBench 检出:默认 `V:\PythonProject\C_UtilizeSpace\LiveBench`,可用环境变量 `DSH_LIVEBENCH_HOME` 覆盖。
56
+ - 已配置好的评测 venv:`<root>\.venv\Scripts\python.exe`(Python 3.11,`pip install -e .` 完成)。
57
+ - API Key:从 harness provider 的 `apiKeyEnv` 环境变量解析,经 `LIVEBENCH_API_KEY` 传给 LiveBench,**不会**出现在命令行或浏览器响应里。
58
+
59
+ ## API(同源)
60
+
61
+ | 路由 | 方法 | 说明 |
62
+ |---|---|---|
63
+ | `/dsh-livebench-panel/api/config` | GET | 可用性、releases、分类→任务表、providers+models |
64
+ | `/dsh-livebench-panel/api/start` | POST | 启动一次 `run_livebench.py` |
65
+ | `/dsh-livebench-panel/api/status` | GET | 运行状态 + 日志尾部 |
66
+ | `/dsh-livebench-panel/api/stop` | POST | 终止当前评测 |
67
+ | `/dsh-livebench-panel/api/results` | GET | 汇总成绩行 |
68
+
69
+ ## 安装(本地开发,遵循 ~/.dsh/AGENTS.md)
70
+
71
+ 1. 插件源码位于 `~/.dsh/plugins/dsh-livebench-panel/`(开发/验证阶段标准位置);
72
+ 2. 在 `~/.dsh/profiles/web/package.json` 的 `dependencies` 加入
73
+ `"dsh-livebench-panel": "link:../../plugins/dsh-livebench-panel"`,
74
+ 并在 `dsh.profile.bundles` 数组加入 `"dsh-livebench-panel"`;
75
+ 3. 在 profile 目录执行 `pnpm install`;
76
+ 4. 重启 `dsh web`,打开任一会话的轨迹视图即可看到 **LiveBench** 标签。
77
+
78
+ 验证通过后可发布 npm,再以 `dsh plugin --profile web add <npm包名>` 正式安装。
@@ -0,0 +1,12 @@
1
+ # dsh-livebench-panel bundle patch: mounts the plugin row into the profile
2
+ # layer stack. The browser half registers a "LiveBench" tab into the
3
+ # Trajectory view (slot conversation.view, id "livebench", order 20 — right
4
+ # of 对话 at order 0 and 轨迹 at order 10); the node half registers the
5
+ # /dsh-livebench-panel/api/* routes that spawn LiveBench runs.
6
+ #
7
+ # Install (local dev, per ~/.dsh/AGENTS.md):
8
+ # profile package.json: "dsh-livebench-panel": "link:../../plugins/dsh-livebench-panel"
9
+ # + add "dsh-livebench-panel" to dsh.profile.bundles, then restart dsh web.
10
+ - insert:
11
+ - id: dsh-livebench-panel
12
+ name: dsh-livebench-panel
package/lib/client.js ADDED
@@ -0,0 +1,363 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-livebench-panel",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ const { createElement: h, useCallback, useEffect, useMemo, useRef, useState } = react;
9
+
10
+ //#region dsh-css:livebench-panel.css
11
+ const css = `.dlb_root{width:100%;max-width:1100px;margin:0 auto;padding:16px 20px 32px;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:14px}
12
+ .dlb_head{display:flex;align-items:baseline;gap:10px}
13
+ .dlb_head h2{margin:0;font-size:16px;font-weight:600}
14
+ .dlb_sub{color:var(--dsw-alias-label-tertiary);font-size:12px}
15
+ .dlb_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:12px}
16
+ .dlb_grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}
17
+ .dlb_field{display:flex;flex-direction:column;gap:4px;min-width:0}
18
+ .dlb_label{color:var(--dsw-alias-label-tertiary);font-size:11px;text-transform:uppercase;letter-spacing:.02em}
19
+ .dlb_select,.dlb_input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;border-radius:8px;padding:0 8px;outline:none;min-width:0}
20
+ .dlb_select:focus-visible,.dlb_input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}
21
+ .dlb_row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
22
+ .dlb_btn{appearance:none;font:inherit;cursor:pointer;border-radius:8px;padding:6px 14px;font-size:13px;border:1px solid transparent;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}
23
+ .dlb_btn:disabled{cursor:default;opacity:.55}
24
+ .dlb_btnGhost{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
25
+ .dlb_btnDanger{background:transparent;border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
26
+ .dlb_badge{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 10px;font-size:11px;color:var(--dsw-alias-label-secondary)}
27
+ .dlb_badge[data-ok="1"]{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 40%,transparent)}
28
+ .dlb_badge[data-ok="0"]{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)}
29
+ .dlb_log{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary)}
30
+ .dlb_table{width:100%;border-collapse:collapse;font-size:12.5px}
31
+ .dlb_table th,.dlb_table td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l2)}
32
+ .dlb_table th{color:var(--dsw-alias-label-tertiary);font-weight:500}
33
+ .dlb_score{font-variant-numeric:tabular-nums;font-weight:600}
34
+ .dlb_error{color:var(--dsw-alias-state-error-primary);font-size:12px;margin:0}
35
+ .dlb_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px;margin:0;line-height:1.5}
36
+ .dlb_notice{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent)}
37
+ .dlb_noticeTitle{margin:0;font-size:13px;font-weight:600;color:var(--dsw-alias-state-error-primary)}
38
+ .dlb_steps{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
39
+ .dlb_code{display:block;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);padding:4px 8px;margin-top:3px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;word-break:break-all;user-select:all}`;
40
+ const tagId = "dsh-livebench-panel/panel.css";
41
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
42
+ const tag = document.createElement("style");
43
+ tag.dataset.plugin = "dsh-livebench-panel";
44
+ tag.dataset.pluginCss = tagId;
45
+ tag.textContent = css;
46
+ document.head.appendChild(tag);
47
+ }
48
+ const c = (n) => "dlb_" + n;
49
+ //#endregion
50
+
51
+ //#region lib/types/client/livebench.js
52
+ const NS = "livebenchPanel";
53
+ const inject = ["slots", "locale"];
54
+
55
+ const API = "/dsh-livebench-panel/api";
56
+
57
+ async function api(path, options) {
58
+ const response = await fetch(API + path, options);
59
+ const payload = await response.json().catch(() => ({ ok: false, error: `HTTP ${response.status}` }));
60
+ return payload;
61
+ }
62
+
63
+ /** Grouped <select> options: one optgroup per harness provider. */
64
+ function modelOptions(providers, selected) {
65
+ const out = [];
66
+ for (const p of providers) {
67
+ if (p.models.length === 0) continue;
68
+ out.push(h("optgroup", { key: p.id, label: p.name + (p.routable ? "" : " ·未接LiveBench") },
69
+ p.models.map((m) => h("option", { key: p.id + "::" + m.id, value: p.id + "::" + m.id }, `${m.name} — ${p.name}`))));
70
+ }
71
+ if (out.length === 0) {
72
+ out.push(h("option", { value: "" }, "(未发现 harness 模型)"));
73
+ }
74
+ void selected;
75
+ return out;
76
+ }
77
+
78
+ function LiveBenchView() {
79
+ const [config, setConfig] = useState(null);
80
+ const [configError, setConfigError] = useState(null);
81
+ const [sel, setSel] = useState({ provider: "", model: "", category: "", task: "", release: "2024-11-25", begin: "", end: "", maxTokens: "4096" });
82
+ const [busy, setBusy] = useState(false);
83
+ const [running, setRunning] = useState(false);
84
+ const [log, setLog] = useState("");
85
+ const [exitCode, setExitCode] = useState(null);
86
+ const [startError, setStartError] = useState(null);
87
+ const [results, setResults] = useState(null);
88
+ const logRef = useRef(null);
89
+
90
+ const loadConfig = useCallback(async () => {
91
+ const payload = await api("/config");
92
+ if (payload.ok) {
93
+ setConfig(payload);
94
+ setConfigError(null);
95
+ setSel((prev) => {
96
+ const next = { ...prev, release: payload.releases.includes(prev.release) ? prev.release : "2024-11-25" };
97
+ const provider = payload.providers.find((p) => p.id === prev.provider) ?? payload.providers.find((p) => p.models.length > 0) ?? null;
98
+ if (provider) {
99
+ next.provider = provider.id;
100
+ if (!provider.models.some((m) => m.id === prev.model)) {
101
+ next.model = provider.models[0]?.id ?? "";
102
+ }
103
+ }
104
+ if (!payload.releases.includes(next.release)) next.release = payload.releases[0] ?? "2024-11-25";
105
+ return next;
106
+ });
107
+ } else {
108
+ setConfigError(payload.error ?? "config unavailable");
109
+ }
110
+ }, []);
111
+
112
+ const loadResults = useCallback(async () => {
113
+ const payload = await api("/results");
114
+ if (payload.ok) setResults(payload);
115
+ }, []);
116
+
117
+ const refreshStatus = useCallback(async () => {
118
+ const payload = await api("/status");
119
+ if (payload.ok) {
120
+ setRunning(payload.running === true);
121
+ if (payload.hasRun) {
122
+ setLog(payload.log ?? "");
123
+ setExitCode(payload.exitCode);
124
+ }
125
+ return payload.running === true;
126
+ }
127
+ return false;
128
+ }, []);
129
+
130
+ useEffect(() => {
131
+ loadConfig();
132
+ loadResults();
133
+ refreshStatus();
134
+ }, [loadConfig, loadResults, refreshStatus]);
135
+
136
+ // poll while a run is active; refresh results when it ends
137
+ const wasRunning = useRef(false);
138
+ useEffect(() => {
139
+ const timer = setInterval(async () => {
140
+ const active = await refreshStatus();
141
+ if (wasRunning.current && !active) loadResults();
142
+ wasRunning.current = active;
143
+ }, 2500);
144
+ return () => clearInterval(timer);
145
+ }, [refreshStatus, loadResults]);
146
+
147
+ useEffect(() => {
148
+ if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
149
+ }, [log]);
150
+
151
+ const categories = useMemo(() => (config ? Object.keys(config.tasks) : []), [config]);
152
+ const taskList = useMemo(() => {
153
+ if (!config || sel.category.length === 0) return [];
154
+ return config.tasks[sel.category] ?? [];
155
+ }, [config, sel.category]);
156
+ const selectedProvider = useMemo(
157
+ () => (config ? config.providers.find((p) => p.id === sel.provider) ?? null : null),
158
+ [config, sel.provider],
159
+ );
160
+ const selectedModel = useMemo(
161
+ () => selectedProvider?.models.find((m) => m.id === sel.model) ?? null,
162
+ [selectedProvider, sel.model],
163
+ );
164
+ const effortOptions = useMemo(() => {
165
+ const efforts = selectedModel?.efforts ?? [];
166
+ return efforts.map((e) => ({ value: e, label: e === "off" ? "off(关闭思考)" : e }));
167
+ }, [selectedModel]);
168
+
169
+ const onStart = async () => {
170
+ setStartError(null);
171
+ setBusy(true);
172
+ try {
173
+ const payload = await api("/start", {
174
+ method: "POST",
175
+ headers: { "content-type": "application/json" },
176
+ body: JSON.stringify({
177
+ provider: sel.provider,
178
+ model: sel.model,
179
+ reasoningEffort: sel.reasoning,
180
+ category: sel.category,
181
+ task: sel.task,
182
+ release: sel.release,
183
+ begin: sel.begin,
184
+ end: sel.end,
185
+ maxTokens: sel.maxTokens,
186
+ }),
187
+ });
188
+ if (!payload.ok) setStartError(payload.error ?? "启动失败");
189
+ else await refreshStatus();
190
+ } catch (error) {
191
+ setStartError(String(error.message ?? error));
192
+ } finally {
193
+ setBusy(false);
194
+ }
195
+ };
196
+
197
+ const onStop = async () => {
198
+ await api("/stop", { method: "POST" });
199
+ await refreshStatus();
200
+ };
201
+
202
+ const setField = (key) => (event) => {
203
+ const value = event.target.value;
204
+ setSel((prev) => {
205
+ const next = { ...prev, [key]: value };
206
+ if (key === "provider") {
207
+ const provider = config?.providers.find((p) => p.id === value) ?? null;
208
+ next.model = provider?.models[0]?.id ?? "";
209
+ }
210
+ if (key === "category") next.task = "";
211
+ return next;
212
+ });
213
+ };
214
+
215
+ // results table: pivot rows -> model x task
216
+ const resultTable = useMemo(() => {
217
+ if (!results || results.rows.length === 0) return null;
218
+ const models = [...new Set(results.rows.map((r) => r.model))].sort();
219
+ const tasks = [...new Set(results.rows.map((r) => r.task))].sort();
220
+ const cell = (model, task) => {
221
+ const row = results.rows.find((r) => r.model === model && r.task === task);
222
+ return row ? `${row.score.toFixed(1)} (${row.judged}/${row.total})` : "—";
223
+ };
224
+ return { models, tasks, cell };
225
+ }, [results]);
226
+
227
+ return h("div", { className: c("root") },
228
+ h("div", { className: c("head") },
229
+ h("h2", null, "LiveBench"),
230
+ h("span", { className: c("sub") }, "LiveBench 评测面板 · 基于 V:\\PythonProject\\C_UtilizeSpace\\LiveBench"),
231
+ h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
232
+ config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
233
+ ),
234
+ configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
235
+ h("div", { className: c("card") },
236
+ h("div", { className: c("grid") },
237
+ h("div", { className: c("field") },
238
+ h("label", { className: c("label") }, "模型(harness 全部模型)"),
239
+ h("select", { className: c("select"), value: sel.provider + "::" + sel.model, onChange: (event) => {
240
+ const [providerId, ...rest] = event.target.value.split("::");
241
+ const modelId = rest.join("::");
242
+ setSel((prev) => ({ ...prev, provider: providerId, model: modelId, reasoning: "default" }));
243
+ } }, modelOptions(config?.providers ?? [])),
244
+ ),
245
+ h("div", { className: c("field") },
246
+ h("label", { className: c("label") }, "推理强度"),
247
+ h("select", { className: c("select"), value: sel.reasoning, onChange: setField("reasoning"), disabled: effortOptions.length === 0 },
248
+ h("option", { value: "default" }, effortOptions.length === 0 ? "(该模型不支持)" : "(模型默认)"),
249
+ effortOptions.map((e) => h("option", { key: e.value, value: e.value }, e.label))),
250
+ ),
251
+ h("div", { className: c("field") },
252
+ h("label", { className: c("label") }, "题集 release"),
253
+ h("select", { className: c("select"), value: sel.release, onChange: setField("release") },
254
+ (config?.releases ?? ["2024-11-25"]).map((r) => h("option", { key: r, value: r }, r))),
255
+ ),
256
+ h("div", { className: c("field") },
257
+ h("label", { className: c("label") }, "分类"),
258
+ h("select", { className: c("select"), value: sel.category, onChange: setField("category") },
259
+ h("option", { value: "" }, "全部分类"),
260
+ categories.map((cat) => h("option", { key: cat, value: cat }, cat))),
261
+ ),
262
+ h("div", { className: c("field") },
263
+ h("label", { className: c("label") }, "任务"),
264
+ h("select", { className: c("select"), value: sel.task, onChange: setField("task") },
265
+ h("option", { value: "" }, sel.category === "" ? "(先选分类)" : "全部任务"),
266
+ taskList.map((task) => h("option", { key: task, value: task }, task))),
267
+ ),
268
+ h("div", { className: c("field") },
269
+ h("label", { className: c("label") }, "题目序号范围(可选)"),
270
+ h("div", { className: c("row") },
271
+ h("input", { className: c("input"), type: "number", min: 0, placeholder: "起", value: sel.begin, onChange: setField("begin"), style: { width: "50%" } }),
272
+ h("input", { className: c("input"), type: "number", min: 0, placeholder: "止", value: sel.end, onChange: setField("end"), style: { width: "50%" } }),
273
+ ),
274
+ ),
275
+ h("div", { className: c("field") },
276
+ h("label", { className: c("label") }, "max-tokens"),
277
+ h("input", { className: c("input"), type: "number", min: 256, max: 32768, value: sel.maxTokens, onChange: setField("maxTokens") }),
278
+ ),
279
+ ),
280
+ h("div", { className: c("row") },
281
+ h("button", { className: c("btn"), onClick: onStart, disabled: busy || running || !config?.available || sel.model === "" },
282
+ running ? "评测运行中…" : busy ? "启动中…" : "开始评测"),
283
+ running && h("button", { className: c("btn") + " " + c("btnDanger"), onClick: onStop }, "停止"),
284
+ h("button", { className: c("btnGhost") + " " + c("btn"), onClick: () => { loadConfig(); loadResults(); } }, "刷新"),
285
+ selectedProvider && !selectedProvider.routable && h("span", { className: c("hint") },
286
+ "该 provider 未配置 openai-completions baseURL,LiveBench 将按模型名原生路由(未注册的模型名会失败)。"),
287
+ ),
288
+ startError !== null && h("p", { className: c("error") }, startError),
289
+ ),
290
+ (log.length > 0 || running) && h("div", { className: c("card") },
291
+ h("div", { className: c("row") },
292
+ h("span", { className: c("badge"), "data-ok": running ? "1" : "0" },
293
+ running ? "运行中" : exitCode === 0 ? "已完成" : exitCode === null ? "待运行" : `已退出 (${exitCode})`),
294
+ ),
295
+ h("pre", { className: c("log"), ref: logRef }, log || "(暂无输出)"),
296
+ ),
297
+ resultTable !== null && h("div", { className: c("card") },
298
+ h("div", { className: c("row") },
299
+ h("span", { className: c("label") }, "评测成绩(分数 = 平均分 ×100,括号内为 已判/总题数)"),
300
+ ),
301
+ h("table", { className: c("table") },
302
+ h("thead", null, h("tr", null,
303
+ h("th", null, "model"),
304
+ resultTable.tasks.map((task) => h("th", { key: task }, task)))),
305
+ h("tbody", null,
306
+ resultTable.models.map((model) => h("tr", { key: model },
307
+ h("td", null, model),
308
+ resultTable.tasks.map((task) => h("td", { key: task, className: c("score") }, resultTable.cell(model, task)))))),
309
+ ),
310
+ ),
311
+ config !== null && config.available === false && h("div", { className: c("card") + " " + c("notice") },
312
+ h("p", { className: c("noticeTitle") }, "⚠ 本面板需要本地安装 LiveBench(当前未检测到)"),
313
+ h("p", { className: c("hint") },
314
+ "dsh-livebench-panel 只是控制台,真正的评测由 LiveBench 项目完成。面板会在以下位置查找它:环境变量 DSH_LIVEBENCH_HOME,或默认路径 V:\\PythonProject\\C_UtilizeSpace\\LiveBench(需含 .venv 与 livebench\\run_livebench.py)。按下面步骤安装:"),
315
+ h("ol", { className: c("steps") },
316
+ h("li", null, "克隆 LiveBench 仓库(放到默认路径可免配置环境变量):",
317
+ h("code", { className: c("code") }, "git clone https://github.com/LiveBench/LiveBench V:\\PythonProject\\C_UtilizeSpace\\LiveBench")),
318
+ h("li", null, "用 Python 3.11 建虚拟环境并安装(3.10 会因 litellm 报 NotRequired 错误):",
319
+ h("code", { className: c("code") }, "cd /d V:\\PythonProject\\C_UtilizeSpace\\LiveBench && py -3.11 -m venv .venv"),
320
+ h("code", { className: c("code") }, ".venv\\Scripts\\python -m pip install -e .")),
321
+ h("li", null, "(评测 coding 类才需要)安装评分依赖:",
322
+ h("code", { className: c("code") }, ".venv\\Scripts\\python -m pip install -r livebench\\code_runner\\requirements_eval.txt")),
323
+ h("li", null, "下载题目数据:",
324
+ h("code", { className: c("code") }, "cd livebench && ..\\.venv\\Scripts\\python download_questions.py")),
325
+ h("li", null, "重启 dsh web,回到本页点「刷新」。",
326
+ h("br", null),
327
+ "若装在其它目录:设置系统环境变量 ", h("code", { className: c("code") }, "DSH_LIVEBENCH_HOME=你的LiveBench目录"), " 后再重启 dsh web。"),
328
+ ),
329
+ h("p", { className: c("hint") }, "更完整的说明见插件目录内 README:~\\.dsh\\plugins\\dsh-livebench-panel\\README.md"),
330
+ ),
331
+ h("p", { className: c("hint") },
332
+ "评分读取 LiveBench 的 ground_truth_judgment.jsonl;正式榜单可用 release 2024-11-25。多选题集请在「分类/任务」中缩小范围,避免长时间运行。"),
333
+ );
334
+ }
335
+
336
+ /**
337
+ * Mount the LiveBench tab into the Trajectory view.
338
+ * @param ctx - the browser plugin context.
339
+ */
340
+ function apply(ctx) {
341
+ ctx.effect(() => ctx.locale.register(NS, {
342
+ zh: { tab: "LiveBench" },
343
+ en: { tab: "LiveBench" },
344
+ }), "livebench-panel: dictionaries");
345
+ const t = ctx.locale.bind(NS);
346
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
347
+ name: "conversation.view",
348
+ id: "livebench",
349
+ order: 20,
350
+ locale: NS,
351
+ label: () => t("tab"),
352
+ inject: () => ({}),
353
+ }, LiveBenchView));
354
+ }
355
+ //#endregion
356
+
357
+ exports.NS = NS;
358
+ exports.inject = inject;
359
+ exports.apply = apply;
360
+ exports.LiveBenchView = LiveBenchView;
361
+ return module.exports;
362
+ }
363
+ });
package/lib/index.js ADDED
@@ -0,0 +1,594 @@
1
+ /**
2
+ * dsh-livebench-panel — node half.
3
+ *
4
+ * Registers same-origin HTTP routes under /dsh-livebench-panel/api/* that
5
+ * bridge the web panel to a local LiveBench checkout:
6
+ *
7
+ * GET /config availability, releases, category→task map, harness models
8
+ * POST /start spawn one `run_livebench.py` evaluation (single run at a time)
9
+ * GET /status running flag + log tail + exit code of the last run
10
+ * POST /stop kill the running evaluation
11
+ * GET /results per-model/per-task mean scores computed from the
12
+ * ground-truth judgment files LiveBench writes on disk
13
+ *
14
+ * The harness model list is read from $DSH_HOME/settings.yaml
15
+ * (llm-pi-ai.providers), which is the same source the model selection UI
16
+ * shows, so the dropdown always mirrors what the harness can serve. API keys
17
+ * are never returned to the browser: the node half resolves `apiKeyEnv` to a
18
+ * process env var and forwards it to LiveBench via LIVEBENCH_API_KEY.
19
+ *
20
+ * LiveBench checkout location (override with env DSH_LIVEBENCH_HOME):
21
+ * V:\PythonProject\C_UtilizeSpace\LiveBench (repo root)
22
+ * <root>\.venv\Scripts\python.exe (evaluation venv, Python 3.11)
23
+ * <root>\livebench (cwd for run_livebench.py)
24
+ *
25
+ * @module dsh-livebench-panel
26
+ */
27
+ import { spawn } from "node:child_process";
28
+ import { readdirSync, readFileSync, existsSync, statSync, writeFileSync } from "node:fs";
29
+ import { createRequire } from "node:module";
30
+ import { homedir } from "node:os";
31
+ import { join } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+
34
+ /** Stable Cordis plugin name. */
35
+ const name = "dsh-livebench-panel";
36
+ /** Services required by this half: the webserver route registry. */
37
+ const inject = ["webServer"];
38
+ /** Route namespace. */
39
+ const API = "/dsh-livebench-panel/api";
40
+ /** Profile whose settings.yaml holds the harness provider config. */
41
+ const PROFILE = "web";
42
+ /** LiveBench checkout used unless DSH_LIVEBENCH_HOME overrides it. */
43
+ const DEFAULT_LIVEBENCH_HOME = "V:\\PythonProject\\C_UtilizeSpace\\LiveBench";
44
+ /** Question-set releases LiveBench accepts (mirrors livebench/common.py). */
45
+ const RELEASES = [
46
+ "2024-06-24", "2024-07-26", "2024-08-31", "2024-11-25",
47
+ "2025-04-02", "2025-04-25", "2025-05-30", "2025-11-25",
48
+ "2025-12-23", "2026-01-08", "2026-06-25",
49
+ ];
50
+ /** Sane cap so a runaway run cannot eat memory with its log. */
51
+ const LOG_MAX_LINES = 600;
52
+ /** One evaluation at a time — LiveBench grading is not cheap. */
53
+ const MAX_CONCURRENT_RUNS = 1;
54
+
55
+ /** Resolve the profile directory from the config-tree anchor (plugin-market pattern). */
56
+ function resolveProfileDir(ctx) {
57
+ if (typeof ctx.baseUrl === "string" && ctx.baseUrl.startsWith("file:")) {
58
+ const path = fileURLToPath(ctx.baseUrl).replace(/[\\/]+$/, "");
59
+ if (existsSync(join(path, "package.json"))) return path;
60
+ }
61
+ const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
62
+ const fallback = join(home, "profiles", PROFILE);
63
+ if (existsSync(join(fallback, "package.json"))) return fallback;
64
+ throw new Error(`${name}: cannot locate the ${PROFILE} profile directory (ctx.baseUrl=${String(ctx.baseUrl)})`);
65
+ }
66
+
67
+ /** Load yaml parser through the profile's own dependency tree. */
68
+ function loadYaml(profileDir) {
69
+ const requireFromProfile = createRequire(join(profileDir, "package.json"));
70
+ return requireFromProfile("yaml");
71
+ }
72
+
73
+ /**
74
+ * Read harness providers from settings.yaml, plus the built-in
75
+ * `deepseek-official` provider (registered by @deepseek-ai/dsh-llm-deepseek,
76
+ * which never appears under llm-pi-ai.providers).
77
+ * @returns {Array<{id: string, name: string, api: string|null, baseURL: string|null, keyEnv: string|null, models: Array<{id: string, name: string, efforts: string[]}>}>}
78
+ */
79
+ function readProviders(profileDir) {
80
+ // profileDir = ~/.dsh/profiles/web → settings.yaml = ~/.dsh/settings.yaml
81
+ const settingsPath = join(profileDir.replace(/[\\/]+$/, ""), "..", "..", "settings.yaml");
82
+ const settings = existsSync(settingsPath) ? loadYaml(profileDir).parse(readFileSync(settingsPath, "utf8")) ?? {} : {};
83
+
84
+ const providers = [];
85
+ const piAi = settings?.["llm-pi-ai"]?.providers ?? {};
86
+ for (const [id, cfg] of Object.entries(piAi)) {
87
+ providers.push({
88
+ id,
89
+ name: typeof cfg?.displayName === "string" && cfg.displayName.length > 0 ? cfg.displayName : id,
90
+ api: typeof cfg?.api === "string" ? cfg.api : null,
91
+ baseURL: typeof cfg?.baseURL === "string" ? cfg.baseURL : null,
92
+ keyEnv: typeof cfg?.apiKeyEnv === "string" ? cfg.apiKeyEnv : null,
93
+ models: Array.isArray(cfg?.models)
94
+ ? cfg.models
95
+ .filter((m) => m && typeof m.id === "string" && m.id.trim().length > 0)
96
+ .map((m) => ({ id: m.id.trim(), name: String(m.name ?? m.id).trim(), efforts: effortsOfModel(m) }))
97
+ : [],
98
+ });
99
+ }
100
+
101
+ // The harness's built-in DeepSeek route (dsh-llm-deepseek). Defaults mirror
102
+ // that package: DEEPSEEK_API_KEY, https://api.deepseek.com, and the
103
+ // off/low/high/max reasoning ladder; user settings may override any of it.
104
+ const dsCfg = settings?.["llm-deepseek"] ?? {};
105
+ const dsModels = Array.isArray(dsCfg.models) && dsCfg.models.length > 0
106
+ ? dsCfg.models
107
+ .filter((m) => m && typeof m.id === "string" && m.id.trim().length > 0)
108
+ .map((m) => ({ id: m.id.trim(), name: String(m.name ?? m.id).trim(), efforts: [...DEEPSEEK_EFFORTS] }))
109
+ : DEFAULT_DEEPSEEK_MODELS.map((m) => ({ ...m, efforts: [...DEEPSEEK_EFFORTS] }));
110
+ providers.push({
111
+ id: "deepseek-official",
112
+ name: "DeepSeek 官方",
113
+ api: "openai-completions",
114
+ baseURL: typeof dsCfg.baseURL === "string" && dsCfg.baseURL.length > 0 ? dsCfg.baseURL : "https://api.deepseek.com",
115
+ keyEnv: typeof dsCfg.apiKeyEnv === "string" && dsCfg.apiKeyEnv.length > 0 ? dsCfg.apiKeyEnv : "DEEPSEEK_API_KEY",
116
+ models: dsModels,
117
+ });
118
+ return providers;
119
+ }
120
+
121
+ /** Reasoning efforts offered by the built-in DeepSeek route. */
122
+ const DEEPSEEK_EFFORTS = ["off", "low", "high", "max"];
123
+ /** Fallback catalog when settings.yaml has no llm-deepseek.models section. */
124
+ const DEFAULT_DEEPSEEK_MODELS = [
125
+ { id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
126
+ { id: "deepseek-v4-pro", name: "DeepSeek-V4-Pro" },
127
+ ];
128
+
129
+ /**
130
+ * Extract the reasoning-effort ladder from a settings.yaml model entry.
131
+ * `reasoningEfforts` maps effort slots to wire values, e.g.
132
+ * `{false: null, low: 'low', high: 'high', max: 'max'}` — a `false` slot whose
133
+ * value is null means "thinking can be turned off"; every non-null value is a
134
+ * selectable effort. Returns wire values, deduplicated, in insertion order.
135
+ */
136
+ function effortsOfModel(model) {
137
+ const efforts = [];
138
+ const seen = new Set();
139
+ if (model && typeof model === "object" && model.reasoningEfforts && typeof model.reasoningEfforts === "object") {
140
+ for (const [slot, wire] of Object.entries(model.reasoningEfforts)) {
141
+ if ((slot === "false" || slot === "true") && (wire === null || wire === undefined)) {
142
+ if (!seen.has("off")) { seen.add("off"); efforts.push("off"); }
143
+ continue;
144
+ }
145
+ const value = String(wire ?? slot);
146
+ if (!seen.has(value)) { seen.add(value); efforts.push(value); }
147
+ }
148
+ }
149
+ return efforts;
150
+ }
151
+
152
+ /** LiveBench layout on disk. */
153
+ function livebenchLayout() {
154
+ const root = process.env.DSH_LIVEBENCH_HOME ?? DEFAULT_LIVEBENCH_HOME;
155
+ const venvPython = join(root, ".venv", "Scripts", "python.exe");
156
+ return {
157
+ root,
158
+ livebenchDir: join(root, "livebench"),
159
+ dataDir: join(root, "livebench", "data", "live_bench"),
160
+ pythonExe: venvPython,
161
+ available: existsSync(venvPython) && existsSync(join(root, "livebench", "run_livebench.py")),
162
+ };
163
+ }
164
+
165
+ /** Scan data/live_bench/<category>/<task>/question.jsonl into a category→tasks map. */
166
+ function scanCategoryTasks(dataDir) {
167
+ const tasks = {};
168
+ if (!existsSync(dataDir)) return tasks;
169
+ let categoryDirs;
170
+ try {
171
+ categoryDirs = readDirSafe(dataDir);
172
+ } catch {
173
+ return tasks;
174
+ }
175
+ for (const category of categoryDirs) {
176
+ const catDir = join(dataDir, category);
177
+ for (const task of readDirSafe(catDir)) {
178
+ if (existsSync(join(catDir, task, "question.jsonl"))) {
179
+ (tasks[category] ??= []).push(task);
180
+ }
181
+ }
182
+ }
183
+ return tasks;
184
+ }
185
+
186
+ function readDirSafe(dir) {
187
+ try {
188
+ return readdirNames(dir);
189
+ } catch {
190
+ return [];
191
+ }
192
+ }
193
+
194
+ function readdirNames(dir) {
195
+ try {
196
+ return readdirSync(dir).filter((entry) => {
197
+ try {
198
+ return statSync(join(dir, entry)).isDirectory();
199
+ } catch {
200
+ return false;
201
+ }
202
+ });
203
+ } catch {
204
+ return [];
205
+ }
206
+ }
207
+
208
+ /** Minimal JSON body reader with a hard size cap. */
209
+ function readBody(req, cap = 16 * 1024) {
210
+ return new Promise((resolve, reject) => {
211
+ let size = 0;
212
+ const chunks = [];
213
+ req.on("data", (chunk) => {
214
+ size += chunk.length;
215
+ if (size > cap) {
216
+ reject(new Error("request body too large"));
217
+ req.destroy();
218
+ return;
219
+ }
220
+ chunks.push(chunk);
221
+ });
222
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
223
+ req.on("error", reject);
224
+ });
225
+ }
226
+
227
+ /** Same-origin guard (plugin-market pattern). */
228
+ function originAllowed(req) {
229
+ const origin = req.headers.origin;
230
+ return origin === undefined || /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin);
231
+ }
232
+
233
+ function sendJson(res, status, payload) {
234
+ res.writeHead(status, { "content-type": "application/json" });
235
+ res.end(JSON.stringify(payload));
236
+ }
237
+
238
+ /** Filename-safe display name for a provider/model pair. */
239
+ function displayModelName(providerId, modelId) {
240
+ return `${String(providerId).replace(/[^A-Za-z0-9._-]/g, "-")}__${String(modelId).replace(/[^A-Za-z0-9._-]/g, "-")}`;
241
+ }
242
+
243
+ /**
244
+ * Write a single-entry model config into LiveBench's model_configs directory so
245
+ * `get_model_config(displayName)` resolves it (instead of the bare custom-model
246
+ * fallback) and the api_kwargs — e.g. `reasoning_effort` — reach the API call.
247
+ * The file is regenerated on every start; secrets never go in here.
248
+ * @returns {string|null} error message, or null on success.
249
+ */
250
+ function writeGeneratedModelConfig(layout, YAML, { displayName, modelId, reasoningEffort }) {
251
+ const configDir = join(layout.livebenchDir, "model", "model_configs");
252
+ const target = join(configDir, "dsh_panel_generated.yaml");
253
+ void YAML;
254
+ const doc = [
255
+ "# Generated by dsh-livebench-panel — regenerated on every evaluation start.",
256
+ "---",
257
+ `display_name: ${displayName}`,
258
+ "api_name:",
259
+ " local: " + modelId,
260
+ "api_kwargs:",
261
+ " default:",
262
+ ` reasoning_effort: ${reasoningEffort}`,
263
+ "",
264
+ ].join("\n");
265
+ try {
266
+ if (!existsSync(configDir)) return `model_configs directory not found: ${configDir}`;
267
+ writeFileSync(target, doc, "utf8");
268
+ return null;
269
+ } catch (error) {
270
+ return `cannot write ${target}: ${error.message}`;
271
+ }
272
+ }
273
+
274
+
275
+ /** Read a JSONL file into objects, skipping broken lines. */
276
+ function readJsonl(path) {
277
+ if (!existsSync(path)) return [];
278
+ try {
279
+ return readFileSync(path, "utf8")
280
+ .split(/\r?\n/)
281
+ .filter((line) => line.trim().length > 0)
282
+ .map((line) => {
283
+ try {
284
+ return JSON.parse(line);
285
+ } catch {
286
+ return null;
287
+ }
288
+ })
289
+ .filter((row) => row !== null);
290
+ } catch {
291
+ return [];
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Compute mean scores per (model, category, task) from ground-truth judgment
297
+ * files, plus the judged/total question counts per task.
298
+ */
299
+ function computeResults(dataDir) {
300
+ const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n}
301
+ const totals = new Map(); // task -> question count
302
+ if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
303
+ for (const category of readDirSafe(dataDir)) {
304
+ const catDir = join(dataDir, category);
305
+ for (const task of readDirSafe(catDir)) {
306
+ const taskDir = join(catDir, task);
307
+ const questions = readJsonl(join(taskDir, "question.jsonl"));
308
+ totals.set(task, questions.length);
309
+ const judgments = readJsonl(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
310
+ for (const row of judgments) {
311
+ const model = typeof row.model === "string" ? row.model : null;
312
+ const score = typeof row.score === "number" ? row.score : Number(row.score);
313
+ if (model === null || !Number.isFinite(score) || score < 0) continue;
314
+ const key = `${model}\u0000${task}`;
315
+ const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0 };
316
+ entry.sum += score;
317
+ entry.n += 1;
318
+ judged.set(key, entry);
319
+ }
320
+ }
321
+ }
322
+ const rows = [...judged.values()].map((entry) => ({
323
+ model: entry.model,
324
+ category: entry.category,
325
+ task: entry.task,
326
+ score: entry.n > 0 ? (entry.sum / entry.n) * 100 : 0,
327
+ judged: entry.n,
328
+ total: totals.get(entry.task) ?? 0,
329
+ }));
330
+ const taskTotals = Object.fromEntries(totals);
331
+ return { rows, taskTotals };
332
+ }
333
+
334
+ /** Clamp helper for query params. */
335
+ function asInt(value, min, max, fallback) {
336
+ const n = Number(value);
337
+ if (!Number.isFinite(n)) return fallback;
338
+ return Math.min(max, Math.max(min, Math.trunc(n)));
339
+ }
340
+
341
+ function apply(ctx) {
342
+ /** The single run slot. */
343
+ let run = null;
344
+
345
+ const startRun = (body) => {
346
+ const layout = livebenchLayout();
347
+ if (!layout.available) {
348
+ return { status: 409, payload: { ok: false, error: `LiveBench venv not found under ${layout.root}` } };
349
+ }
350
+ if (run && run.exitCode === null) {
351
+ return { status: 409, payload: { ok: false, error: "another evaluation is already running" } };
352
+ }
353
+ const providerId = typeof body.provider === "string" ? body.provider : "";
354
+ const modelId = typeof body.model === "string" ? body.model.trim() : "";
355
+ const release = typeof body.release === "string" ? body.release : "2024-11-25";
356
+ const category = typeof body.category === "string" && body.category.length > 0 ? body.category : null;
357
+ const task = typeof body.task === "string" && body.task.length > 0 ? body.task : null;
358
+ const reasoningEffort = typeof body.reasoningEffort === "string" && body.reasoningEffort.length > 0 && body.reasoningEffort !== "default" ? body.reasoningEffort : null;
359
+ if (modelId.length === 0 || modelId.length > 120 || /[^\w.:\/_-]/.test(modelId)) {
360
+ return { status: 400, payload: { ok: false, error: "invalid model id" } };
361
+ }
362
+ if (reasoningEffort !== null && !/^[a-z0-9_-]{1,20}$/.test(reasoningEffort)) {
363
+ return { status: 400, payload: { ok: false, error: `invalid reasoning effort: ${reasoningEffort}` } };
364
+ }
365
+ if (!RELEASES.includes(release)) {
366
+ return { status: 400, payload: { ok: false, error: `unknown release: ${release}` } };
367
+ }
368
+
369
+ const profileDir = resolveProfileDir(ctx);
370
+ const providers = readProviders(profileDir);
371
+ const provider = providers.find((p) => p.id === providerId) ?? null;
372
+
373
+ // A selected reasoning effort is passed through LiveBench's model-config
374
+ // api_kwargs (chat_completion_openai merges them into the SDK call). The
375
+ // effort is encoded in the display name so different efforts land in
376
+ // separate result rows.
377
+ const hasEffortSuffix = reasoningEffort !== null && reasoningEffort !== "off";
378
+ const displayName = displayModelName(providerId || "direct", modelId) + (hasEffortSuffix ? "@" + reasoningEffort : "");
379
+ let cliModel = modelId;
380
+ if (hasEffortSuffix) {
381
+ const writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), { displayName, modelId, reasoningEffort });
382
+ if (writeError) return { status: 500, payload: { ok: false, error: writeError } };
383
+ cliModel = displayName;
384
+ }
385
+
386
+ const benchParts = ["live_bench", ...(category ? [category] : []), ...(task ? [task] : [])];
387
+ const args = [
388
+ "run_livebench.py",
389
+ "--model", cliModel,
390
+ "--model-display-name", displayName,
391
+ "--bench-name", benchParts.join("/"),
392
+ "--livebench-release-option", release,
393
+ "--max-tokens", String(asInt(body.maxTokens, 256, 32768, 4096)),
394
+ "--parallel-requests", String(asInt(body.parallel, 1, 8, 2)),
395
+ "--mode", "single",
396
+ ];
397
+ if (body.begin !== undefined && body.begin !== null && `${body.begin}`.length > 0) {
398
+ args.push("--question-begin", String(asInt(body.begin, 0, 100000, 0)));
399
+ }
400
+ if (body.end !== undefined && body.end !== null && `${body.end}`.length > 0) {
401
+ args.push("--question-end", String(asInt(body.end, 0, 100000, 0)));
402
+ }
403
+ if (body.resume === true) args.push("--resume");
404
+
405
+ const env = {
406
+ ...process.env,
407
+ HF_HOME: join(layout.root, ".hf_cache"),
408
+ HF_HUB_DISABLE_SYMLINKS_WARNING: "1",
409
+ };
410
+ // Only OpenAI-compatible providers can be routed with --api-base; for
411
+ // those, hand the key over via env (never the command line).
412
+ if (provider && provider.api === "openai-completions" && provider.baseURL) {
413
+ args.push("--api-base", provider.baseURL);
414
+ if (provider.keyEnv && process.env[provider.keyEnv]) {
415
+ env.LIVEBENCH_API_KEY = process.env[provider.keyEnv];
416
+ }
417
+ }
418
+
419
+ const child = spawn(layout.pythonExe, args, {
420
+ cwd: layout.livebenchDir,
421
+ env,
422
+ windowsHide: true,
423
+ });
424
+
425
+ const record = {
426
+ id: `${Date.now()}`,
427
+ proc: child,
428
+ exitCode: null,
429
+ startedAt: new Date().toISOString(),
430
+ command: [layout.pythonExe, ...args].join(" "),
431
+ log: [`$ ${[...args].join(" ")}`],
432
+ };
433
+ run = record;
434
+
435
+ child.stdout.on("data", (chunk) => appendLog(record, chunk));
436
+ child.stderr.on("data", (chunk) => appendLog(record, chunk));
437
+ child.on("error", (error) => {
438
+ appendLog(record, `[spawn error] ${error.message}`);
439
+ record.exitCode = -1;
440
+ });
441
+ child.on("close", (code) => {
442
+ record.exitCode = code === null ? -1 : code;
443
+ record.log.push(`[exit ${record.exitCode}]`);
444
+ });
445
+
446
+ return { status: 200, payload: { ok: true, runId: record.id, displayName, command: record.command } };
447
+ };
448
+
449
+ const appendLog = (record, chunk) => {
450
+ for (const line of String(chunk).split(/\r?\n/)) {
451
+ if (line.length === 0) continue;
452
+ record.log.push(line);
453
+ }
454
+ if (record.log.length > LOG_MAX_LINES) {
455
+ record.log.splice(0, record.log.length - LOG_MAX_LINES);
456
+ }
457
+ };
458
+
459
+ /**
460
+ * Mount the routes.
461
+ * @param ctx - plugin context carrying the webServer service.
462
+ */
463
+ ctx.effect(() => ctx.webServer.register({
464
+ kind: "exact",
465
+ path: `${API}/config`,
466
+ handler: async (req, res) => {
467
+ if (req.method !== "GET") {
468
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
469
+ return;
470
+ }
471
+ const layout = livebenchLayout();
472
+ let providers = [];
473
+ try {
474
+ providers = readProviders(resolveProfileDir(ctx));
475
+ } catch (error) {
476
+ providers = [];
477
+ appendLog({ log: [] }, `[config] settings.yaml unreadable: ${error.message}`);
478
+ }
479
+ sendJson(res, 200, {
480
+ ok: true,
481
+ available: layout.available,
482
+ root: layout.root,
483
+ releases: RELEASES,
484
+ tasks: scanCategoryTasks(layout.dataDir),
485
+ providers: providers.map(({ id, name: pname, models, api, baseURL }) => ({
486
+ id,
487
+ name: pname,
488
+ routable: api === "openai-completions" && typeof baseURL === "string" && baseURL.length > 0,
489
+ models,
490
+ })),
491
+ });
492
+ },
493
+ }), `${name}: config route`);
494
+
495
+ ctx.effect(() => ctx.webServer.register({
496
+ kind: "exact",
497
+ path: `${API}/start`,
498
+ handler: async (req, res) => {
499
+ if (req.method !== "POST") {
500
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
501
+ return;
502
+ }
503
+ if (!originAllowed(req)) {
504
+ sendJson(res, 403, { ok: false, error: "forbidden origin" });
505
+ return;
506
+ }
507
+ let body;
508
+ try {
509
+ body = JSON.parse(await readBody(req));
510
+ } catch {
511
+ sendJson(res, 400, { ok: false, error: "invalid request body" });
512
+ return;
513
+ }
514
+ if (run !== null && run.exitCode === null && runsAlive() >= MAX_CONCURRENT_RUNS) {
515
+ sendJson(res, 409, { ok: false, error: "another evaluation is already running" });
516
+ return;
517
+ }
518
+ const result = startRun(body);
519
+ sendJson(res, result.status, result.payload);
520
+ },
521
+ }), `${name}: start route`);
522
+
523
+ ctx.effect(() => ctx.webServer.register({
524
+ kind: "exact",
525
+ path: `${API}/status`,
526
+ handler: async (req, res) => {
527
+ if (req.method !== "GET") {
528
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
529
+ return;
530
+ }
531
+ if (run === null) {
532
+ sendJson(res, 200, { ok: true, running: false, hasRun: false });
533
+ return;
534
+ }
535
+ sendJson(res, 200, {
536
+ ok: true,
537
+ running: run.exitCode === null,
538
+ hasRun: true,
539
+ runId: run.id,
540
+ exitCode: run.exitCode,
541
+ startedAt: run.startedAt,
542
+ command: run.command,
543
+ log: run.log.slice(-120).join("\n"),
544
+ });
545
+ },
546
+ }), `${name}: status route`);
547
+
548
+ ctx.effect(() => ctx.webServer.register({
549
+ kind: "exact",
550
+ path: `${API}/stop`,
551
+ handler: async (req, res) => {
552
+ if (req.method !== "POST") {
553
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
554
+ return;
555
+ }
556
+ if (!originAllowed(req)) {
557
+ sendJson(res, 403, { ok: false, error: "forbidden origin" });
558
+ return;
559
+ }
560
+ if (run === null || run.exitCode !== null) {
561
+ sendJson(res, 200, { ok: true, stopped: false });
562
+ return;
563
+ }
564
+ try {
565
+ run.proc.kill();
566
+ } catch (error) {
567
+ sendJson(res, 500, { ok: false, error: String(error.message ?? error) });
568
+ return;
569
+ }
570
+ sendJson(res, 200, { ok: true, stopped: true });
571
+ },
572
+ }), `${name}: stop route`);
573
+
574
+ ctx.effect(() => ctx.webServer.register({
575
+ kind: "exact",
576
+ path: `${API}/results`,
577
+ handler: async (req, res) => {
578
+ if (req.method !== "GET") {
579
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
580
+ return;
581
+ }
582
+ const layout = livebenchLayout();
583
+ const { rows, taskTotals } = computeResults(layout.dataDir);
584
+ sendJson(res, 200, { ok: true, rows, taskTotals });
585
+ },
586
+ }), `${name}: results route`);
587
+ }
588
+
589
+ /** Count alive runs (at most one today, kept for future parallel lanes). */
590
+ function runsAlive() {
591
+ return 1;
592
+ }
593
+
594
+ export { name, inject, apply, writeGeneratedModelConfig, readProviders };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "dsh-livebench-panel",
3
+ "version": "0.1.1",
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
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./cordis.patch.yml": "./cordis.patch.yml",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "cordis.patch.yml",
20
+ "README.md"
21
+ ],
22
+ "keywords": [
23
+ "dsh-plugin",
24
+ "deepseek-harness",
25
+ "livebench",
26
+ "benchmark",
27
+ "evaluation"
28
+ ],
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ },
33
+ "client": {
34
+ "platform": "web",
35
+ "inject": [
36
+ "@deepseek-ai/dsh-client-runtime",
37
+ "@deepseek-ai/dsh-client-locale",
38
+ "@deepseek-ai/dsh-client-ui-slots",
39
+ "@deepseek-ai/dsh-client-ui-conversation"
40
+ ]
41
+ }
42
+ }
43
+ }