dsh-livebench-panel 0.1.7 → 0.1.9

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.
Files changed (3) hide show
  1. package/lib/client.js +99 -67
  2. package/lib/index.js +167 -45
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -98,10 +98,12 @@ window.__ModuleLoader__.load({
98
98
  const [log, setLog] = useState("");
99
99
  const [exitCode, setExitCode] = useState(null);
100
100
  const [startError, setStartError] = useState(null);
101
+ const [homeInput, setHomeInput] = useState("");
102
+ const [homeBusy, setHomeBusy] = useState(false);
101
103
  const [results, setResults] = useState(null);
102
- const [pickedTasks, setPickedTasks] = useState(() => new Set());
103
- const [taskOrder, setTaskOrder] = useState(() => {
104
- try { return JSON.parse(localStorage.getItem("dlb_task_order_v1")) ?? []; } catch { return []; }
104
+ const [pickedModels, setPickedModels] = useState(() => new Set());
105
+ const [modelOrder, setModelOrder] = useState(() => {
106
+ try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
105
107
  });
106
108
  const dragKey = useRef(null);
107
109
  const logRef = useRef(null);
@@ -253,6 +255,24 @@ window.__ModuleLoader__.load({
253
255
  await refreshStatus();
254
256
  };
255
257
 
258
+ const saveHome = async () => {
259
+ setHomeBusy(true);
260
+ setStartError(null);
261
+ try {
262
+ const payload = await api("/home", {
263
+ method: "POST",
264
+ headers: { "content-type": "application/json" },
265
+ body: JSON.stringify({ path: homeInput }),
266
+ });
267
+ if (!payload.ok) setStartError(payload.error ?? "设置失败");
268
+ else { setHomeInput(""); await loadConfig(); }
269
+ } catch (error) {
270
+ setStartError(String(error.message ?? error));
271
+ } finally {
272
+ setHomeBusy(false);
273
+ }
274
+ };
275
+
256
276
  const setField = (key) => (event) => {
257
277
  const value = event.target.value;
258
278
  setSel((prev) => {
@@ -265,78 +285,75 @@ window.__ModuleLoader__.load({
265
285
  });
266
286
  };
267
287
 
268
- // ---- 成绩矩阵:task 为行(含 category 行标)、model 为列 ----
269
- // 附带:time 列(该任务评测开始时间 = 行内最早判分时间)、
288
+ // ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
289
+ // 附带:time 列(该模型评测开始时间 = 行内最早判分时间)、
270
290
  // 行拖拽排序(localStorage 持久化)、行选择与单行/批量删除。
271
291
  const taskKey = (category, task) => `${category}/${task}`;
272
292
  const matrix = useMemo(() => {
273
293
  if (!results) return null;
274
294
  const models = [...new Set(results.rows.map((r) => r.model))].sort();
275
- const byTask = new Map(); // `${cat}/${task}` -> {category, task, cells: Map(model->row), startTime}
295
+ const tasks = [...new Set(results.rows.map((r) => taskKey(r.category, r.task)))].sort();
296
+ const cells = new Map(); // `${model}\u0000${taskKey}` -> row
297
+ const startTimes = new Map(); // model -> min tstamp
276
298
  for (const row of results.rows) {
277
- const key = taskKey(row.category, row.task);
278
- const entry = byTask.get(key) ?? { category: row.category, task: row.task, cells: new Map(), startTime: Infinity };
279
- entry.cells.set(row.model, row);
280
- if (row.time && row.time < entry.startTime) entry.startTime = row.time;
281
- byTask.set(key, entry);
299
+ cells.set(`${row.model}\u0000${taskKey(row.category, row.task)}`, row);
300
+ const prev = startTimes.get(row.model);
301
+ if (row.time && (prev === undefined || row.time < prev)) startTimes.set(row.model, row.time);
282
302
  }
283
- return { models, tasks: [...byTask.entries()] };
303
+ return { models, tasks, cells, startTimes };
284
304
  }, [results]);
285
- const sortedTasks = useMemo(() => {
305
+ const sortedModels = useMemo(() => {
286
306
  if (!matrix) return [];
287
- const indexOf = new Map(taskOrder.map((key, index) => [key, index]));
288
- return [...matrix.tasks].sort((a, b) => {
289
- const ia = indexOf.has(a[0]) ? indexOf.get(a[0]) : Number.MAX_SAFE_INTEGER;
290
- const ib = indexOf.has(b[0]) ? indexOf.get(b[0]) : Number.MAX_SAFE_INTEGER;
307
+ const indexOf = new Map(modelOrder.map((key, index) => [key, index]));
308
+ return [...matrix.models].sort((a, b) => {
309
+ const ia = indexOf.has(a) ? indexOf.get(a) : Number.MAX_SAFE_INTEGER;
310
+ const ib = indexOf.has(b) ? indexOf.get(b) : Number.MAX_SAFE_INTEGER;
291
311
  if (ia !== ib) return ia - ib;
292
- return a[0].localeCompare(b[0]);
312
+ return a.localeCompare(b);
293
313
  });
294
- }, [matrix, taskOrder]);
314
+ }, [matrix, modelOrder]);
295
315
 
296
- const persistTaskOrder = (keys) => {
297
- setTaskOrder(keys);
298
- try { localStorage.setItem("dlb_task_order_v1", JSON.stringify(keys)); } catch { /* private mode */ }
316
+ const persistModelOrder = (keys) => {
317
+ setModelOrder(keys);
318
+ try { localStorage.setItem("dlb_model_order_v1", JSON.stringify(keys)); } catch { /* private mode */ }
299
319
  };
300
320
 
301
- const onRowDrop = (targetKey) => {
302
- const sourceKey = dragKey.current;
321
+ const onRowDrop = (targetModel) => {
322
+ const sourceModel = dragKey.current;
303
323
  dragKey.current = null;
304
- if (!sourceKey || sourceKey === targetKey) return;
305
- const keys = sortedTasks.map(([key]) => key);
306
- const from = keys.indexOf(sourceKey);
307
- const to = keys.indexOf(targetKey);
324
+ if (!sourceModel || sourceModel === targetModel) return;
325
+ const keys = sortedModels.slice();
326
+ const from = keys.indexOf(sourceModel);
327
+ const to = keys.indexOf(targetModel);
308
328
  if (from < 0 || to < 0) return;
309
329
  keys.splice(to, 0, keys.splice(from, 1)[0]);
310
- persistTaskOrder(keys);
330
+ persistModelOrder(keys);
311
331
  };
312
332
 
313
- const togglePicked = (key) => setPickedTasks((prev) => {
333
+ const togglePicked = (model) => setPickedModels((prev) => {
314
334
  const next = new Set(prev);
315
- if (next.has(key)) next.delete(key); else next.add(key);
335
+ if (next.has(model)) next.delete(model); else next.add(model);
316
336
  return next;
317
337
  });
318
338
 
319
- // 删除一个任务行 = 删除该任务下所有模型的判分与答案记录
320
- const deleteTasks = async (keys) => {
321
- if (!results || keys.length === 0) return;
339
+ // 删除一个模型行 = 删除该模型在所有任务下的判分与答案记录
340
+ const deleteModels = async (modelsToDelete) => {
341
+ if (!results || modelsToDelete.length === 0) return;
322
342
  const serverRows = [];
323
- for (const key of keys) {
324
- const [category, task] = key.split("/");
325
- for (const row of results.rows) {
326
- if (row.category === category && row.task === task) {
327
- serverRows.push({ model: row.model, category, task });
328
- }
343
+ for (const row of results.rows) {
344
+ if (modelsToDelete.includes(row.model)) {
345
+ serverRows.push({ model: row.model, category: row.category, task: row.task });
329
346
  }
330
347
  }
331
348
  if (serverRows.length === 0) return;
332
- if (!window.confirm(`确认删除所选 ${keys.length} 个任务的全部评测成绩(共 ${serverRows.length} 条模型记录,含答案)?不可恢复。`)) return;
349
+ if (!window.confirm(`确认删除所选 ${modelsToDelete.length} 个模型的全部评测成绩(共 ${serverRows.length} 条记录,含答案)?不可恢复。`)) return;
333
350
  await api("/delete", {
334
351
  method: "POST",
335
352
  headers: { "content-type": "application/json" },
336
353
  body: JSON.stringify({ rows: serverRows }),
337
354
  });
338
- setPickedTasks(new Set());
339
- persistTaskOrder(taskOrder.filter((key) => !keys.includes(key)));
355
+ setPickedModels(new Set());
356
+ persistModelOrder(modelOrder.filter((model) => !modelsToDelete.includes(model)));
340
357
  await loadResults();
341
358
  };
342
359
 
@@ -350,10 +367,21 @@ window.__ModuleLoader__.load({
350
367
  return h("div", { className: c("root") },
351
368
  h("div", { className: c("head") },
352
369
  h("h2", null, "LiveBench"),
353
- h("span", { className: c("sub") }, "LiveBench 评测面板 · 基于 V:\\PythonProject\\C_UtilizeSpace\\LiveBench"),
370
+ h("span", { className: c("sub") }, config?.root ? `LiveBench 评测面板 · ${config.root}` : "LiveBench 评测面板"),
354
371
  h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
355
372
  config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
356
373
  ),
374
+ h("div", { className: c("row") },
375
+ h("span", { className: c("label") }, "LiveBench 路径"),
376
+ h("input", {
377
+ className: c("input"), style: { flex: "1", minWidth: "240px" },
378
+ placeholder: config?.root ?? "V:\\…\\LiveBench 或 ~/LiveBench",
379
+ value: homeInput, onChange: (event) => setHomeInput(event.target.value),
380
+ }),
381
+ h("button", { className: c("btnGhost") + " " + c("btn"), disabled: homeBusy || homeInput.trim().length === 0, onClick: saveHome },
382
+ homeBusy ? "保存中…" : "设置路径"),
383
+ h("span", { className: c("hint") }, "需含 livebench\\run_livebench.py 与 .venv;也可用环境变量 DSH_LIVEBENCH_HOME"),
384
+ ),
357
385
  configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
358
386
  h("div", { className: c("card") },
359
387
  h("div", { className: c("grid") },
@@ -420,7 +448,7 @@ window.__ModuleLoader__.load({
420
448
  running && h("button", { className: c("btn") + " " + c("btnDanger"), onClick: onStop }, "停止"),
421
449
  h("button", { className: c("btnGhost") + " " + c("btn"), onClick: () => { loadConfig(); loadResults(); } }, "刷新"),
422
450
  selectedProvider && !selectedProvider.routable && h("span", { className: c("hint") },
423
- "该 provider 未配置 openai-completions baseURL,LiveBench 将按模型名原生路由(未注册的模型名会失败)。"),
451
+ "该 provider 的协议或端点未知,无法自动路由:LiveBench 将按模型名原生尝试,未注册的模型名会失败。"),
424
452
  ),
425
453
  startError !== null && h("p", { className: c("error") }, startError),
426
454
  ),
@@ -431,49 +459,53 @@ window.__ModuleLoader__.load({
431
459
  ),
432
460
  h("pre", { className: c("log"), ref: logRef }, log || "(暂无输出)"),
433
461
  ),
434
- sortedTasks.length > 0 && h("div", { className: c("card") },
462
+ sortedModels.length > 0 && h("div", { className: c("card") },
435
463
  h("div", { className: c("row") },
436
- h("span", { className: c("label") }, "评测成绩(分数 = 平均分 ×100,括号内为 已判/总题数;行首可拖动排序,顺序保存在本机)"),
464
+ h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
437
465
  h("button", {
438
466
  className: c("btn") + " " + c("btnDanger"),
439
- disabled: pickedTasks.size === 0,
440
- onClick: () => deleteTasks([...pickedTasks]),
441
- }, `删除所选(${pickedTasks.size})`),
467
+ disabled: pickedModels.size === 0,
468
+ onClick: () => deleteModels([...pickedModels]),
469
+ }, `删除所选(${pickedModels.size})`),
442
470
  ),
443
471
  h("table", { className: c("table") },
444
472
  h("thead", null, h("tr", null,
445
473
  h("th", { style: { width: "26px" } }, "⠿"),
446
474
  h("th", { style: { width: "30px" } },
447
- h("input", { type: "checkbox", checked: pickedTasks.size > 0 && pickedTasks.size === sortedTasks.length,
448
- onChange: (event) => setPickedTasks(event.target.checked ? new Set(sortedTasks.map(([key]) => key)) : new Set()) })),
449
- h("th", null, "task"),
450
- matrix.models.map((model) => h("th", { key: model }, model)),
475
+ h("input", { type: "checkbox", checked: pickedModels.size > 0 && pickedModels.size === sortedModels.length,
476
+ onChange: (event) => setPickedModels(event.target.checked ? new Set(sortedModels) : new Set()) })),
477
+ h("th", null, "model"),
478
+ matrix.tasks.map((taskKeyCol) => {
479
+ const [cat, task] = taskKeyCol.split("/");
480
+ return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
481
+ }),
451
482
  h("th", null, "删除"),
452
483
  h("th", null, "time"),
453
484
  )),
454
485
  h("tbody", null,
455
- sortedTasks.map(([key, entry]) => {
456
- const checked = pickedTasks.has(key);
486
+ sortedModels.map((model) => {
487
+ const checked = pickedModels.has(model);
488
+ const startTime = matrix.startTimes.get(model);
457
489
  return h("tr", {
458
- key,
490
+ key: model,
459
491
  draggable: true,
460
- onDragStart: () => { dragKey.current = key; },
492
+ onDragStart: () => { dragKey.current = model; },
461
493
  onDragOver: (event) => event.preventDefault(),
462
- onDrop: () => onRowDrop(key),
494
+ onDrop: () => onRowDrop(model),
463
495
  },
464
496
  h("td", { className: c("drag"), title: "拖动排序" }, "⠿"),
465
- h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(key) })),
466
- h("td", null, h("span", { title: `${entry.category} / ${entry.task}` }, `${entry.category} / ${entry.task}`)),
467
- matrix.models.map((model) => {
468
- const row = entry.cells.get(model);
469
- return h("td", { key: model, className: c("score") },
497
+ h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(model) })),
498
+ h("td", null, model),
499
+ matrix.tasks.map((taskKeyCol) => {
500
+ const row = matrix.cells.get(`${model}\u0000${taskKeyCol}`);
501
+ return h("td", { key: taskKeyCol, className: c("score") },
470
502
  row ? `${row.score.toFixed(1)} (${row.judged}/${row.total})` : "—");
471
503
  }),
472
504
  h("td", null, h("button", {
473
- className: c("btnGhost") + " " + c("btn"), title: "删除该任务下所有模型的评测成绩",
474
- onClick: () => deleteTasks([key]),
505
+ className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
506
+ onClick: () => deleteModels([model]),
475
507
  }, "删除")),
476
- h("td", null, fmtTime(entry.startTime === Infinity ? null : entry.startTime)),
508
+ h("td", null, fmtTime(startTime)),
477
509
  );
478
510
  })),
479
511
  ),
package/lib/index.js CHANGED
@@ -39,8 +39,61 @@ const inject = ["webServer"];
39
39
  const API = "/dsh-livebench-panel/api";
40
40
  /** Profile whose settings.yaml holds the harness provider config. */
41
41
  const PROFILE = "web";
42
- /** LiveBench checkout used unless DSH_LIVEBENCH_HOME overrides it. */
43
- const DEFAULT_LIVEBENCH_HOME = "V:\\PythonProject\\C_UtilizeSpace\\LiveBench";
42
+ /** LiveBench location resolution: saved user path > env var > user home > legacy. */
43
+ const CONFIG_FILE = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "livebench-panel.json");
44
+ const LEGACY_LIVEBENCH_HOME = "V:\\PythonProject\\C_UtilizeSpace\\LiveBench";
45
+
46
+ /** Read the user-saved LiveBench path (set from the panel UI). */
47
+ function savedLivebenchHome() {
48
+ try {
49
+ const path = JSON.parse(readFileSync(CONFIG_FILE, "utf8")).livebenchHome;
50
+ return typeof path === "string" && path.trim().length > 0 ? path.trim() : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** Candidate roots, most explicit first. Windows and Linux both covered. */
57
+ function livebenchCandidates() {
58
+ return [
59
+ savedLivebenchHome(),
60
+ process.env.DSH_LIVEBENCH_HOME ?? null,
61
+ join(homedir(), "LiveBench"),
62
+ LEGACY_LIVEBENCH_HOME,
63
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
64
+ }
65
+
66
+ /** A root counts as a LiveBench checkout when the venv python and entry script exist. */
67
+ function isLivebenchRoot(root) {
68
+ return existsSync(join(root, ".venv", "Scripts", "python.exe")) || existsSync(join(root, ".venv", "bin", "python"))
69
+ ? existsSync(join(root, "livebench", "run_livebench.py"))
70
+ : false;
71
+ }
72
+
73
+ /** Resolve the LiveBench layout from the first valid candidate. */
74
+ function livebenchLayout() {
75
+ const candidates = livebenchCandidates();
76
+ for (const root of candidates) {
77
+ const pythonExe = join(root, ".venv", "Scripts", process.platform === "win32" ? "python.exe" : "python");
78
+ if (existsSync(pythonExe) && existsSync(join(root, "livebench", "run_livebench.py"))) {
79
+ return {
80
+ root,
81
+ livebenchDir: join(root, "livebench"),
82
+ dataDir: join(root, "livebench", "data", "live_bench"),
83
+ pythonExe,
84
+ available: true,
85
+ };
86
+ }
87
+ }
88
+ const root = candidates[0] ?? join(homedir(), "LiveBench");
89
+ return {
90
+ root,
91
+ livebenchDir: join(root, "livebench"),
92
+ dataDir: join(root, "livebench", "data", "live_bench"),
93
+ pythonExe: join(root, ".venv", process.platform === "win32" ? join("Scripts", "python.exe") : join("bin", "python")),
94
+ available: false,
95
+ };
96
+ }
44
97
  /** Question-set releases LiveBench accepts (mirrors livebench/common.py). */
45
98
  const RELEASES = [
46
99
  "2024-06-24", "2024-07-26", "2024-08-31", "2024-11-25",
@@ -123,10 +176,10 @@ function readProviders(profileDir) {
123
176
  if (piAiData !== null) {
124
177
  for (const provider of providers) {
125
178
  if (provider.baseURL === null) {
126
- const base = builtinBaseUrl(piAiData, provider.id);
127
- if (base !== null) {
128
- provider.baseURL = base;
129
- provider.api = provider.api ?? "openai-completions";
179
+ const info = builtinProtocolInfo(piAiData, provider.id);
180
+ if (info) {
181
+ provider.baseURL = info.baseURL;
182
+ provider.api = info.api;
130
183
  }
131
184
  }
132
185
  }
@@ -162,17 +215,25 @@ function resolvePiAiDataDir(profileDir) {
162
215
  return null;
163
216
  }
164
217
 
165
- /** Read one provider's built-in OpenAI-compatible baseUrl from pi-ai data. */
166
- function builtinBaseUrl(piAiDataDir, providerId) {
218
+ /**
219
+ * Read one provider's built-in protocol + baseUrl from pi-ai data.
220
+ * Prefers OpenAI Chat Completions; Anthropic Messages is also routable
221
+ * (LiveBench talks it natively and honors ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY).
222
+ */
223
+ function builtinProtocolInfo(piAiDataDir, providerId) {
167
224
  if (!/^[a-z0-9-]+$/.test(providerId)) return null;
168
225
  const file = join(piAiDataDir, `${providerId}.json`);
169
226
  if (!existsSync(file)) return null;
170
227
  try {
171
228
  const data = JSON.parse(readFileSync(file, "utf8"));
172
- const openai = data?.["openai-completions"];
173
- if (openai && typeof openai === "object") {
174
- for (const entry of Object.values(openai)) {
175
- if (entry && typeof entry.baseUrl === "string" && entry.baseUrl.length > 0) return entry.baseUrl;
229
+ for (const api of ["openai-completions", "anthropic-messages"]) {
230
+ const group = data?.[api];
231
+ if (group && typeof group === "object") {
232
+ for (const entry of Object.values(group)) {
233
+ if (entry && typeof entry.baseUrl === "string" && entry.baseUrl.length > 0) {
234
+ return { api, baseURL: entry.baseUrl };
235
+ }
236
+ }
176
237
  }
177
238
  }
178
239
  } catch { /* unreadable data file — treat as unknown */ }
@@ -210,19 +271,6 @@ function effortsOfModel(model) {
210
271
  return efforts;
211
272
  }
212
273
 
213
- /** LiveBench layout on disk. */
214
- function livebenchLayout() {
215
- const root = process.env.DSH_LIVEBENCH_HOME ?? DEFAULT_LIVEBENCH_HOME;
216
- const venvPython = join(root, ".venv", "Scripts", "python.exe");
217
- return {
218
- root,
219
- livebenchDir: join(root, "livebench"),
220
- dataDir: join(root, "livebench", "data", "live_bench"),
221
- pythonExe: venvPython,
222
- available: existsSync(venvPython) && existsSync(join(root, "livebench", "run_livebench.py")),
223
- };
224
- }
225
-
226
274
  /** Scan data/live_bench/<category>/<task>/question.jsonl into a category→tasks map. */
227
275
  function scanCategoryTasks(dataDir) {
228
276
  const tasks = {};
@@ -315,21 +363,32 @@ function displayModelName(providerId, modelId) {
315
363
  * The file is regenerated on every start; secrets never go in here.
316
364
  * @returns {string|null} error message, or null on success.
317
365
  */
318
- function writeGeneratedModelConfig(layout, YAML, { displayName, modelId, reasoningEffort }) {
366
+ function writeGeneratedModelConfig(layout, YAML, { displayName, modelId, reasoningEffort, protocol }) {
319
367
  const configDir = join(layout.livebenchDir, "model", "model_configs");
320
368
  const target = join(configDir, "dsh_panel_generated.yaml");
321
369
  void YAML;
322
- const doc = [
370
+ const lines = [
323
371
  "# Generated by dsh-livebench-panel — regenerated on every evaluation start.",
324
372
  "---",
325
373
  `display_name: ${displayName}`,
326
374
  "api_name:",
327
- " local: " + modelId,
328
- "api_kwargs:",
329
- " default:",
330
- ` reasoning_effort: ${reasoningEffort}`,
331
- "",
332
- ].join("\n");
375
+ ];
376
+ if (protocol === "anthropic") {
377
+ // Route through LiveBench's native anthropic client; the endpoint is
378
+ // pointed at the provider proxy via ANTHROPIC_BASE_URL (spawn env).
379
+ lines.push(` anthropic: ${modelId}`, "default_provider: anthropic");
380
+ if (reasoningEffort) {
381
+ // note: reasoning_effort is an OpenAI-style knob and is intentionally
382
+ // not forwarded on the anthropic protocol path.
383
+ }
384
+ } else {
385
+ lines.push(` local: ${modelId}`);
386
+ if (reasoningEffort) {
387
+ lines.push("api_kwargs:", " default:", ` reasoning_effort: ${reasoningEffort}`);
388
+ }
389
+ }
390
+ lines.push("");
391
+ const doc = lines.join("\n");
333
392
  try {
334
393
  if (!existsSync(configDir)) return `model_configs directory not found: ${configDir}`;
335
394
  writeFileSync(target, doc, "utf8");
@@ -492,8 +551,19 @@ function apply(ctx) {
492
551
  const hasEffortSuffix = reasoningEffort !== null && reasoningEffort !== "off";
493
552
  const displayName = displayModelName(providerId || "direct", modelId) + (hasEffortSuffix ? "@" + reasoningEffort : "");
494
553
  let cliModel = modelId;
495
- if (hasEffortSuffix) {
496
- const writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), { displayName, modelId, reasoningEffort });
554
+ let writeError = null;
555
+ // Anthropic-protocol proxies cannot go through --api-base (that path
556
+ // speaks OpenAI Chat Completions). Instead the generated model config
557
+ // selects LiveBench's native anthropic client and the spawn env points
558
+ // the SDK at the proxy (ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY).
559
+ const isAnthropicRoute = provider && provider.api === "anthropic-messages" && provider.baseURL;
560
+ if (isAnthropicRoute || hasEffortSuffix) {
561
+ writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), {
562
+ displayName,
563
+ modelId,
564
+ reasoningEffort: isAnthropicRoute ? null : reasoningEffort,
565
+ protocol: isAnthropicRoute ? "anthropic" : "openai",
566
+ });
497
567
  if (writeError) return { status: 500, payload: { ok: false, error: writeError } };
498
568
  cliModel = displayName;
499
569
  }
@@ -527,15 +597,13 @@ function apply(ctx) {
527
597
  // livebench import (shortuuid etc.).
528
598
  PATH: `${join(layout.root, ".venv", "Scripts")}${delimiter}${process.env.PATH ?? ""}`,
529
599
  };
530
- // Only OpenAI-compatible providers can be routed with --api-base; for
531
- // those, hand the key over via env (never the command line). The key is
532
- // resolved through the harness credential seam when available (values may
533
- // live encrypted in .credentials.yaml rather than in the process env),
534
- // falling back to the plain environment variable.
535
- if (provider && provider.api === "openai-completions" && provider.baseURL) {
536
- args.push("--api-base", provider.baseURL);
600
+ // Provider routing: keys are resolved through the harness credential seam
601
+ // when available (values may live encrypted in .credentials.yaml rather
602
+ // than in the process env), falling back to the plain environment
603
+ // variable. Secrets travel via env only never the command line.
604
+ if (provider && provider.baseURL) {
605
+ let key;
537
606
  if (provider.keyEnv) {
538
- let key;
539
607
  try {
540
608
  const credentials = ctx.get ? ctx.get("credentials") : undefined;
541
609
  if (credentials && typeof credentials.resolve === "function") {
@@ -544,7 +612,15 @@ function apply(ctx) {
544
612
  }
545
613
  } catch { /* credential seam unavailable — fall through to env */ }
546
614
  if (!key && process.env[provider.keyEnv]) key = process.env[provider.keyEnv];
615
+ }
616
+ if (provider.api === "openai-completions") {
617
+ args.push("--api-base", provider.baseURL);
547
618
  if (key) env.LIVEBENCH_API_KEY = key;
619
+ } else if (provider.api === "anthropic-messages") {
620
+ // The generated model config selects the native anthropic client;
621
+ // the Anthropic SDK picks endpoint+key up from these env vars.
622
+ if (key) env.ANTHROPIC_API_KEY = key;
623
+ env.ANTHROPIC_BASE_URL = provider.baseURL;
548
624
  }
549
625
  }
550
626
 
@@ -617,7 +693,7 @@ function apply(ctx) {
617
693
  providers: providers.map(({ id, name: pname, models, api, baseURL }) => ({
618
694
  id,
619
695
  name: pname,
620
- routable: api === "openai-completions" && typeof baseURL === "string" && baseURL.length > 0,
696
+ routable: (api === "openai-completions" || api === "anthropic-messages") && typeof baseURL === "string" && baseURL.length > 0,
621
697
  baseURL: baseURL ?? null,
622
698
  models,
623
699
  })),
@@ -695,7 +771,14 @@ function apply(ctx) {
695
771
  return;
696
772
  }
697
773
  try {
698
- run.proc.kill();
774
+ if (process.platform === "win32") {
775
+ // proc.kill() only terminates run_livebench.py itself; its child
776
+ // (cmd → gen_api_answer.py → …) would survive. taskkill /T /F
777
+ // takes down the whole tree.
778
+ spawn("taskkill", ["/PID", String(run.proc.pid), "/T", "/F"], { windowsHide: true });
779
+ } else {
780
+ run.proc.kill();
781
+ }
699
782
  } catch (error) {
700
783
  sendJson(res, 500, { ok: false, error: String(error.message ?? error) });
701
784
  return;
@@ -718,6 +801,45 @@ function apply(ctx) {
718
801
  },
719
802
  }), `${name}: results route`);
720
803
 
804
+ ctx.effect(() => ctx.webServer.register({
805
+ kind: "exact",
806
+ path: `${API}/home`,
807
+ handler: async (req, res) => {
808
+ if (req.method !== "POST") {
809
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
810
+ return;
811
+ }
812
+ if (!originAllowed(req)) {
813
+ sendJson(res, 403, { ok: false, error: "forbidden origin" });
814
+ return;
815
+ }
816
+ let body;
817
+ try {
818
+ body = JSON.parse(await readBody(req));
819
+ } catch {
820
+ sendJson(res, 400, { ok: false, error: "invalid request body" });
821
+ return;
822
+ }
823
+ const path = typeof body.path === "string" ? body.path.trim() : "";
824
+ if (path.length === 0 || path.length > 260 || /[<>:"|?*\0]/.test(path.replace(/^[A-Za-z]:/, ""))) {
825
+ sendJson(res, 400, { ok: false, error: "invalid path" });
826
+ return;
827
+ }
828
+ if (!existsSync(join(path, "livebench", "run_livebench.py"))) {
829
+ sendJson(res, 400, { ok: false, error: `该目录下没有 livebench\\run_livebench.py:${path}` });
830
+ return;
831
+ }
832
+ try {
833
+ writeFileSync(CONFIG_FILE, JSON.stringify({ livebenchHome: path }, null, 2), "utf8");
834
+ } catch (error) {
835
+ sendJson(res, 500, { ok: false, error: `cannot save config: ${error.message}` });
836
+ return;
837
+ }
838
+ const layout = livebenchLayout();
839
+ sendJson(res, 200, { ok: true, root: layout.root, available: layout.available });
840
+ },
841
+ }), `${name}: home route`);
842
+
721
843
  ctx.effect(() => ctx.webServer.register({
722
844
  kind: "exact",
723
845
  path: `${API}/delete`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "DSH web plugin: a LiveBench tab in the Trajectory view (right of 对话/轨迹). Run LiveBench evaluations against every model configured in the DeepSeek Harness — pick provider/model, category, task, release and question range from dropdowns, watch progress, and read scores in place.",
5
5
  "license": "MIT",
6
6
  "type": "module",