dsh-fingerprint 0.3.1 → 0.4.0
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 +8 -17
- package/client.js +101 -1
- package/docs/DESIGN.md +21 -16
- package/docs/screenshot.png +0 -0
- package/index.js +80 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,10 +16,12 @@ dsh-fingerprint 把 DSH 里已经配置好的 `provider/model` 路由当作审
|
|
|
16
16
|
|
|
17
17
|
插件不保存 provider API key;认证和请求路由由 DSH 的 `llm` 服务处理。无法关闭 reasoning 的模型会被硬拦截,因为论文协议需要一次直接的单次 completion。
|
|
18
18
|
|
|
19
|
+
这是一个手动使用的插件:正常操作都在 Web 面板里完成,默认不向 agent 注册工具,因此不占用模型上下文。需要让 agent 驱动采集做调试或自动化时,在面板底部的“设置”里把 Agent 工具开关打开即可,即时生效,不需要改配置文件。
|
|
20
|
+
|
|
19
21
|
## 要求
|
|
20
22
|
|
|
21
|
-
- 一个启用 Web GUI 的 DSH profile(需要 `llm`、`
|
|
22
|
-
- DSH 0.2.1
|
|
23
|
+
- 一个启用 Web GUI 的 DSH profile(需要 `llm`、`webServer` 服务;开启 agent 工具时还需要 `tools`)。
|
|
24
|
+
- DSH 0.2.1 系列:安装、面板、API 与端到端采集已在 `0.2.1-rc.2` 和 `0.2.1-rc.4` 上验证;面板的 Agent 工具开关和 settings API 在 `0.2.1-rc.5` 上实测。`engines.dsh` 限定 `>=0.2.1-rc.2 <0.2.2-0`。
|
|
23
25
|
- 至少一个配置好的、能关闭 reasoning(`off` / `none`)或本身不推理的 provider/model。
|
|
24
26
|
- Node.js(由 DSH 自带,无需单独安装)。
|
|
25
27
|
|
|
@@ -53,19 +55,7 @@ dsh plugin --profile web add github:Jerry2262/dsh-fingerprint
|
|
|
53
55
|
4. 在“对比记录”里选两条记录,点“对比记录”。比较方式只有一种,结果会标注两边是否同一目标。
|
|
54
56
|
5. 结果包含差异分数、判定线、结论、关系、cell 级差异和警告。低于判定线只说明差异小,不等于 100% 是同一个模型。
|
|
55
57
|
6. 行内垃圾桶可以删除记录;删除前会二次确认。运行中的记录不能删除。
|
|
56
|
-
|
|
57
|
-
## Agent 工具
|
|
58
|
-
|
|
59
|
-
Host 服务同时暴露给 agent:
|
|
60
|
-
|
|
61
|
-
- `llm_fingerprint_list_models`
|
|
62
|
-
- `llm_fingerprint_probe`
|
|
63
|
-
- `llm_fingerprint_capture`
|
|
64
|
-
- `llm_fingerprint_get_record`
|
|
65
|
-
- `llm_fingerprint_compare`
|
|
66
|
-
- `llm_fingerprint_delete_record`
|
|
67
|
-
|
|
68
|
-
UI 和工具调用同一个 Host 服务,不维护第二套逻辑。
|
|
58
|
+
7. 面板底部的“设置”里有 Agent 工具开关,默认关闭。开启后把 6 个 `llm_fingerprint_*` 工具暴露给 agent(每个请求多约 4.3 KB 工具 schema),关闭后立即从后续请求里移除;选择保存在数据目录的 `settings.json`,删掉这个文件即回到默认关闭。
|
|
69
59
|
|
|
70
60
|
## 工作原理
|
|
71
61
|
|
|
@@ -79,6 +69,7 @@ UI 和工具调用同一个 Host 服务,不维护第二套逻辑。
|
|
|
79
69
|
|
|
80
70
|
- 插件不保存 API key;key 留在 DSH 的 provider/credential 层。
|
|
81
71
|
- 原始响应和指纹默认写到 `$DSH_HOME/llm-fingerprint/`,也可以由插件 row 的 `dataDir` 覆盖。
|
|
72
|
+
- 面板设置(目前只有 Agent 工具开关)存在同一数据目录的 `settings.json`。
|
|
82
73
|
- 删除记录会删除该目录下的 `meta.json`、`responses.jsonl` 和 `fingerprint.json`。
|
|
83
74
|
- 无法关闭 reasoning 的模型不会被静默采集。
|
|
84
75
|
|
|
@@ -93,14 +84,14 @@ UI 和工具调用同一个 Host 服务,不维护第二套逻辑。
|
|
|
93
84
|
|
|
94
85
|
## 开发
|
|
95
86
|
|
|
96
|
-
- `index.js`:Host plugin,`llm` / `webServer` service、
|
|
87
|
+
- `index.js`:Host plugin,`llm` / `webServer` service、HTTP API;面板开关开启时另注册 6 个 agent 工具。
|
|
97
88
|
- `core.js`:协议常量、提示词 battery、归一化、JSD、指纹与对比。
|
|
98
89
|
- `client.js`:Web 面板(`main` + `sidebar.panellist`)、详情抽屉、对比与删除确认。
|
|
99
90
|
- `prompts.json`:论文 paper-1 的 10 个任务 × 4 种语言。
|
|
100
91
|
- `locale/`:插件元数据和 UI 文案。
|
|
101
92
|
- `docs/DESIGN.md`:设计细节。
|
|
102
93
|
- `npm run check`:JS 语法检查(无构建步骤)。
|
|
103
|
-
- `npm test
|
|
94
|
+
- `npm test`:核心与插件注册回归测试(判定线、覆盖门槛、协议拒绝、重复 cell、主要答案,以及 agent 工具默认关闭/开关注册)。
|
|
104
95
|
|
|
105
96
|
## 许可与来源
|
|
106
97
|
|
package/client.js
CHANGED
|
@@ -120,6 +120,11 @@ window.__ModuleLoader__.load({
|
|
|
120
120
|
referenceValidity: 'valid rate 97.6% overall, per-model median 99.6%',
|
|
121
121
|
referenceSplitHalf: 'split-half JSD 0.140 for the same model, 0.463 for different models',
|
|
122
122
|
referenceScore: 'The cutoff is 0.280, the 1st percentile of different-model distances: in 100 different models, about 1 would still look the same.',
|
|
123
|
+
settingsTitle: 'Settings',
|
|
124
|
+
toolsTitle: 'Agent tools',
|
|
125
|
+
toolsHint: 'Expose the six capture and compare tools to agents. Adds about 4.3 KB of tool schema to every request; useful for debugging or automation.',
|
|
126
|
+
toolsUnavailable: 'This profile does not provide the tools service, so the switch cannot be turned on.',
|
|
127
|
+
toolsUpdated: 'Agent tool visibility updated.',
|
|
123
128
|
};
|
|
124
129
|
|
|
125
130
|
const zh = {
|
|
@@ -234,6 +239,11 @@ window.__ModuleLoader__.load({
|
|
|
234
239
|
referenceValidity: '有效率总体 97.6%,模型内中位 99.6%',
|
|
235
240
|
referenceSplitHalf: '同一模型的分半差异通常 0.140,不同模型通常 0.463',
|
|
236
241
|
referenceScore: '判定线是 0.280,取自不同模型距离的 1% 分位:100 个不同模型里,大约只有 1 个会被看成相同。',
|
|
242
|
+
settingsTitle: '设置',
|
|
243
|
+
toolsTitle: 'Agent 工具',
|
|
244
|
+
toolsHint: '把 6 个采集、对比工具暴露给 agent。每个请求会多约 4.3 KB 工具 schema,适合调试或自动化。',
|
|
245
|
+
toolsUnavailable: '当前 profile 没有 tools 服务,无法开启这个开关。',
|
|
246
|
+
toolsUpdated: 'agent 工具开关已更新。',
|
|
237
247
|
};
|
|
238
248
|
|
|
239
249
|
async function api(path, init) {
|
|
@@ -307,6 +317,7 @@ window.__ModuleLoader__.load({
|
|
|
307
317
|
if (/still running/i.test(text)) return t('errorStillRunning');
|
|
308
318
|
if (/unknown fingerprint record/i.test(text)) return t('errorRecordNotFound');
|
|
309
319
|
if (/no completed fingerprint|two record ids|record id is required/i.test(text)) return t('errorRecordNeedsFingerprint');
|
|
320
|
+
if (/tools service is unavailable/i.test(text)) return t('toolsUnavailable');
|
|
310
321
|
return text;
|
|
311
322
|
}
|
|
312
323
|
|
|
@@ -444,6 +455,39 @@ window.__ModuleLoader__.load({
|
|
|
444
455
|
fontSize: '11px',
|
|
445
456
|
cursor: 'pointer',
|
|
446
457
|
},
|
|
458
|
+
settingRow: {
|
|
459
|
+
display: 'flex',
|
|
460
|
+
alignItems: 'center',
|
|
461
|
+
justifyContent: 'space-between',
|
|
462
|
+
gap: '20px',
|
|
463
|
+
},
|
|
464
|
+
switch: {
|
|
465
|
+
position: 'relative',
|
|
466
|
+
flex: 'none',
|
|
467
|
+
width: '38px',
|
|
468
|
+
height: '22px',
|
|
469
|
+
padding: 0,
|
|
470
|
+
borderRadius: '999px',
|
|
471
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
472
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
473
|
+
cursor: 'pointer',
|
|
474
|
+
transition: 'background .15s ease, border-color .15s ease',
|
|
475
|
+
},
|
|
476
|
+
switchOn: {
|
|
477
|
+
background: 'var(--dsw-alias-brand-primary)',
|
|
478
|
+
borderColor: 'var(--dsw-alias-brand-primary)',
|
|
479
|
+
},
|
|
480
|
+
switchKnob: {
|
|
481
|
+
position: 'absolute',
|
|
482
|
+
top: '2px',
|
|
483
|
+
left: '2px',
|
|
484
|
+
width: '16px',
|
|
485
|
+
height: '16px',
|
|
486
|
+
borderRadius: '50%',
|
|
487
|
+
background: '#fff',
|
|
488
|
+
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.3)',
|
|
489
|
+
transition: 'transform .15s ease',
|
|
490
|
+
},
|
|
447
491
|
iconButton: {
|
|
448
492
|
display: 'inline-flex',
|
|
449
493
|
alignItems: 'center',
|
|
@@ -707,6 +751,8 @@ window.__ModuleLoader__.load({
|
|
|
707
751
|
const [detail, setDetail] = useState(null);
|
|
708
752
|
const [detailId, setDetailId] = useState('');
|
|
709
753
|
const [compact, setCompact] = useState(false);
|
|
754
|
+
const [toolsSetting, setToolsSetting] = useState(null);
|
|
755
|
+
const [savingTools, setSavingTools] = useState(false);
|
|
710
756
|
const resultRef = useRef(null);
|
|
711
757
|
const pageRef = useRef(null);
|
|
712
758
|
|
|
@@ -738,8 +784,13 @@ window.__ModuleLoader__.load({
|
|
|
738
784
|
let cancelled = false;
|
|
739
785
|
(async () => {
|
|
740
786
|
try {
|
|
741
|
-
const [targetData, checkpointData] = await Promise.all([
|
|
787
|
+
const [targetData, checkpointData, settingsData] = await Promise.all([
|
|
788
|
+
api('/targets'),
|
|
789
|
+
api('/checkpoints'),
|
|
790
|
+
api('/settings').catch(() => null),
|
|
791
|
+
]);
|
|
742
792
|
if (cancelled) return;
|
|
793
|
+
if (settingsData) setToolsSetting(settingsData.tools || null);
|
|
743
794
|
const providers = (targetData.providers || []).filter((entry) => (entry.models || []).length > 0);
|
|
744
795
|
setTargets(providers);
|
|
745
796
|
setCheckpoints(checkpointData.checkpoints || []);
|
|
@@ -894,6 +945,25 @@ window.__ModuleLoader__.load({
|
|
|
894
945
|
}
|
|
895
946
|
}
|
|
896
947
|
|
|
948
|
+
async function toggleTools() {
|
|
949
|
+
if (!toolsSetting || savingTools) return;
|
|
950
|
+
setSavingTools(true);
|
|
951
|
+
setError('');
|
|
952
|
+
setNotice('');
|
|
953
|
+
try {
|
|
954
|
+
const data = await api('/settings/tools', {
|
|
955
|
+
method: 'POST',
|
|
956
|
+
body: JSON.stringify({ enabled: !toolsSetting.enabled }),
|
|
957
|
+
});
|
|
958
|
+
setToolsSetting(data.tools);
|
|
959
|
+
setNotice(t('toolsUpdated'));
|
|
960
|
+
} catch (cause) {
|
|
961
|
+
setError(friendlyError(cause.message || cause, t));
|
|
962
|
+
} finally {
|
|
963
|
+
setSavingTools(false);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
897
967
|
const estimate = 40 * Math.max(10, Math.min(100, Number(reps) || 30));
|
|
898
968
|
|
|
899
969
|
return h('div', { style: S.page },
|
|
@@ -1144,6 +1214,36 @@ window.__ModuleLoader__.load({
|
|
|
1144
1214
|
: null,
|
|
1145
1215
|
) : null,
|
|
1146
1216
|
),
|
|
1217
|
+
|
|
1218
|
+
h(Section, { title: t('settingsTitle') },
|
|
1219
|
+
h('div', { style: S.settingRow },
|
|
1220
|
+
h('div', { style: { minWidth: 0 } },
|
|
1221
|
+
h('div', { style: { fontSize: '13px', fontWeight: 600 } }, t('toolsTitle')),
|
|
1222
|
+
h('p', { style: { ...S.hint, margin: '4px 0 0' } }, t('toolsHint')),
|
|
1223
|
+
toolsSetting && !toolsSetting.available
|
|
1224
|
+
? h('p', { style: { ...S.hint, margin: '4px 0 0', color: 'var(--dsw-alias-state-warn-primary)' } }, t('toolsUnavailable'))
|
|
1225
|
+
: null,
|
|
1226
|
+
),
|
|
1227
|
+
h('button', {
|
|
1228
|
+
type: 'button',
|
|
1229
|
+
role: 'switch',
|
|
1230
|
+
'aria-checked': Boolean(toolsSetting && toolsSetting.enabled),
|
|
1231
|
+
'aria-label': t('toolsTitle'),
|
|
1232
|
+
disabled: !toolsSetting || savingTools || !toolsSetting.available,
|
|
1233
|
+
style: {
|
|
1234
|
+
...S.switch,
|
|
1235
|
+
...(toolsSetting && toolsSetting.enabled ? S.switchOn : {}),
|
|
1236
|
+
...(!toolsSetting || savingTools || !toolsSetting.available ? S.disabled : {}),
|
|
1237
|
+
},
|
|
1238
|
+
onClick: toggleTools,
|
|
1239
|
+
}, h('span', {
|
|
1240
|
+
style: {
|
|
1241
|
+
...S.switchKnob,
|
|
1242
|
+
transform: toolsSetting && toolsSetting.enabled ? 'translateX(16px)' : 'none',
|
|
1243
|
+
},
|
|
1244
|
+
})),
|
|
1245
|
+
),
|
|
1246
|
+
),
|
|
1147
1247
|
),
|
|
1148
1248
|
detail ? h('div', {
|
|
1149
1249
|
style: S.drawerOverlay,
|
package/docs/DESIGN.md
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
- **审计目标**:DSH 已配置的一条 provider 路由 + 一个 model id。插件不维护自己的端点表,也不保存 API key。
|
|
8
8
|
- **指纹记录**:对某个目标在某一时刻做一次完整采集的结果。记录本身就是指纹,包含 40 个 cell 的经验答案分布和派生统计。
|
|
9
9
|
- **对比**:任意两条记录之间的同一个操作。跨模型、跨服务商、同一目标的前后两次,都是同一个比较操作,只有 `relation` 标签不同。
|
|
10
|
-
- **单一实现**:Host service 是唯一操作层;
|
|
10
|
+
- **单一实现**:Host service 是唯一操作层;Web 面板通过 HTTP API 调用,可选的 agent 工具调用同一组方法。
|
|
11
|
+
- **默认手动**:插件默认不向 agent 注册工具,所有操作在面板里完成,因此不占用模型上下文;面板“设置”里的开关打开时才注册 6 个 agent 工具,选择持久化在数据目录的 `settings.json`。
|
|
11
12
|
|
|
12
13
|
## 2. 协议常量
|
|
13
14
|
|
|
14
15
|
论文:Bruckner, *One Token Is Enough*,arXiv:2607.10252。
|
|
15
16
|
|
|
16
17
|
- 10 个任务 × 4 种语言(en / ru / zh / ar)= 40 个 cell。
|
|
17
|
-
- 每个 cell 默认 30 次重复;UI 最低 10,API
|
|
18
|
+
- 每个 cell 默认 30 次重复;UI 最低 10,HTTP API 和可选的 agent 工具允许更低用于诊断。
|
|
18
19
|
- `temperature=1.0`,`max_tokens=16`,每个 cell 使用对应语言的固定单词 system prompt。
|
|
19
20
|
- reasoning 必须关闭;不能关闭的模型不进入采集。
|
|
20
21
|
- 答案归一化:Unicode NFC、去标点/引号、case fold、阿拉伯-印度数字和中文数字映射、取首个词;每条响应归为 `valid` / `invalid` / `refusal` / `empty`,带 reasoning trace 的标为 `post_reasoning` 并从有效分布中排除。
|
|
@@ -36,6 +37,7 @@ llm-fingerprint/
|
|
|
36
37
|
responses.jsonl # 每条成功请求一行,append-only
|
|
37
38
|
fingerprint.json # 派生分布、valid rate、分半一致性、cell 明细
|
|
38
39
|
probes/<probe-id>.json
|
|
40
|
+
settings.json # 面板设置(目前只有 agent 工具开关)
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
- 记录 id 形如 `cp-20261020T034450-59c80896`,服务端用严格格式校验,防止路径穿越。
|
|
@@ -48,18 +50,19 @@ llm-fingerprint/
|
|
|
48
50
|
|
|
49
51
|
## 4. Host 结构
|
|
50
52
|
|
|
51
|
-
`index.js` 通过 `apply(ctx, config)`
|
|
53
|
+
`index.js` 通过 `apply(ctx, config)` 注册一个 HTTP API,prefix `/llm-fingerprint/api`,供 Web 面板调用:
|
|
52
54
|
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
- `GET /targets`
|
|
56
|
+
- `GET /checkpoints`
|
|
57
|
+
- `POST /checkpoints`
|
|
58
|
+
- `GET /checkpoints/:id`
|
|
59
|
+
- `DELETE /checkpoints/:id`
|
|
60
|
+
- `POST /compare`
|
|
61
|
+
- `POST /probe`
|
|
62
|
+
- `GET /settings`
|
|
63
|
+
- `POST /settings/tools`
|
|
64
|
+
|
|
65
|
+
所有操作都在 `createService(ctx, root)` 返回的同一组方法里实现。面板“设置”里的 Agent 工具开关调用 `POST /settings/tools`:Host 按需通过 `ctx.inject(['tools'], ...)` 注册 6 个 agent 工具(`llm_fingerprint_list_models` / `probe` / `capture` / `get_record` / `compare` / `delete_record`),关闭时 dispose 该子 fiber 立即注销;默认关闭,因此不要求 profile 提供 `tools` 服务。开关选择持久化在数据目录的 `settings.json`,插件重载后恢复。
|
|
63
66
|
|
|
64
67
|
目标发现:
|
|
65
68
|
|
|
@@ -87,7 +90,7 @@ llm-fingerprint/
|
|
|
87
90
|
|
|
88
91
|
- UI 模型下拉里仍然列出,但 `disabled`,文本标注“不可关闭推理”;
|
|
89
92
|
- 服务商下所有模型都不可用时,服务商选项标注“无可用模型”,模型选择禁用,Start 禁用,并显示原因;
|
|
90
|
-
- Host `startCheckpoint` 再检查一次,API 和 agent 工具都无法绕过;
|
|
93
|
+
- Host `startCheckpoint` 再检查一次,HTTP API 和 agent 工具都无法绕过;
|
|
91
94
|
- `probe` 仍然可以用于诊断,但不会创建记录。
|
|
92
95
|
|
|
93
96
|
这套判断完全由运行时配置/适配器元数据计算,不维护模型名黑名单。给某个模型在 DSH 配置里加上 `off` 映射后,它会自动变为可用,不需要改插件代码。
|
|
@@ -124,11 +127,12 @@ llm-fingerprint/
|
|
|
124
127
|
|
|
125
128
|
## 8. 限制与不做的事
|
|
126
129
|
|
|
127
|
-
- 兼容范围:`engines.dsh` 限定 DSH 0.2.1 系列(`>=0.2.1-rc.2 <0.2.2-0
|
|
130
|
+
- 兼容范围:`engines.dsh` 限定 DSH 0.2.1 系列(`>=0.2.1-rc.2 <0.2.2-0`);安装、面板、API 与一次端到端采集已在 rc.2 和 rc.4 验证,面板 Agent 工具开关与 settings API 在 rc.5 上实测。0.2.2 及以后需重新测试再放宽。
|
|
128
131
|
- 不做 post-reasoning 指纹模式;只能 reasoning 的模型被拒绝。
|
|
129
132
|
- 不读取/保存 provider key;认证由 DSH 适配器负责。
|
|
130
133
|
- 不读取 provider 返回的 model 字符串或聚合器上游路由;`ctx.llm.stream` 不暴露这些。
|
|
131
134
|
- 不把“分布不一致”解释成“供应商故意掺水”;它只是可观察行为差异。
|
|
135
|
+
- agent 工具默认关闭,由面板“设置”里的开关控制;开启后 6 个工具定义会进入每个请求的上下文(实测约 4.3 KB / 1.2K tokens),这是调试或自动化的成本,不是面板的依赖。
|
|
132
136
|
- 不实现自动定时巡检、多端点距离矩阵、CSV 导出、自定义 battery 编辑器;这些是后续功能。
|
|
133
137
|
|
|
134
138
|
## 9. 维护
|
|
@@ -136,4 +140,5 @@ llm-fingerprint/
|
|
|
136
140
|
- 提示词版本写在 `prompts.json` 的 `version` 和每条记录的 `promptVersion`;改提示词需要重新采集,旧记录不能直接和新记录混比。
|
|
137
141
|
- 判定线常量在 `core.js` 的 `CUTOFF`;修改时需要同步 README/DESIGN 并说明校准来源。
|
|
138
142
|
- 数据目录 append-only;分析产物可以删除后从 `responses.jsonl` 重新计算。
|
|
139
|
-
-
|
|
143
|
+
- 面板设置只有 agent 工具开关,写在数据目录的 `settings.json`;删除该文件即恢复默认关闭,不影响记录数据。
|
|
144
|
+
- 代码无构建步骤;`npm run check` 做三个 JS 文件的语法检查,`npm test` 跑核心与插件注册回归测试。
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* the per-cell empirical answer distribution. Any two records compare with the
|
|
7
7
|
* same operation and the same decision cutoff.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The service methods back the HTTP API that the Web panel calls; there is one
|
|
10
|
+
* implementation of every operation. The panel is the normal surface; its
|
|
11
|
+
* settings section can optionally expose the six agent tools, which stay out of
|
|
12
|
+
* the model context while the toggle is off (the default).
|
|
12
13
|
*/
|
|
13
14
|
import { randomUUID } from 'node:crypto';
|
|
14
15
|
import {
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
topAnswerOf,
|
|
39
40
|
} from './core.js';
|
|
40
41
|
|
|
41
|
-
export const inject = ['llm', '
|
|
42
|
+
export const inject = ['llm', 'webServer'];
|
|
42
43
|
|
|
43
44
|
const CONCURRENCY = 8;
|
|
44
45
|
const REQUEST_TIMEOUT_MS = 90_000;
|
|
@@ -926,6 +927,68 @@ function toolDefinitions(service) {
|
|
|
926
927
|
];
|
|
927
928
|
}
|
|
928
929
|
|
|
930
|
+
const AGENT_TOOLS_SETTINGS_FILE = 'settings.json';
|
|
931
|
+
|
|
932
|
+
/** Read the persisted panel preferences; unreadable or malformed files fall back to the defaults. */
|
|
933
|
+
function readSettings(file) {
|
|
934
|
+
try {
|
|
935
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
936
|
+
return { tools: value?.tools === true };
|
|
937
|
+
} catch {
|
|
938
|
+
return { tools: false };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Persist the panel preferences. Write failures surface to the caller instead of being swallowed. */
|
|
943
|
+
function writeSettings(file, settings) {
|
|
944
|
+
writeFileSync(file, `${JSON.stringify({ tools: settings.tools === true }, null, 2)}\n`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Own the optional agent tool surface: the six tools are registered exactly
|
|
949
|
+
* while the Web panel's toggle is on.
|
|
950
|
+
*
|
|
951
|
+
* The setting persists in the plugin's data directory (`settings.json`) and
|
|
952
|
+
* defaults to off, so a fresh install adds no tool definitions to the model
|
|
953
|
+
* context. Registration waits for the `tools` service through `ctx.inject`;
|
|
954
|
+
* turning the toggle off disposes that child fiber, which unregisters the tools
|
|
955
|
+
* immediately — no profile reload or DSH restart involved.
|
|
956
|
+
*/
|
|
957
|
+
function createAgentTools(ctx, service, root) {
|
|
958
|
+
const file = path.join(root, AGENT_TOOLS_SETTINGS_FILE);
|
|
959
|
+
let settings = readSettings(file);
|
|
960
|
+
let fiber;
|
|
961
|
+
|
|
962
|
+
const status = () => ({ enabled: settings.tools, available: Boolean(ctx.get('tools')) });
|
|
963
|
+
const start = () => {
|
|
964
|
+
if (fiber !== undefined) return;
|
|
965
|
+
fiber = ctx.inject(['tools'], function registerFingerprintTools(toolCtx) {
|
|
966
|
+
for (const definition of toolDefinitions(service)) {
|
|
967
|
+
toolCtx.effect(() => toolCtx.tools.register(definition), `llm-fingerprint: ${definition.name}`);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
};
|
|
971
|
+
const stop = async () => {
|
|
972
|
+
const current = fiber;
|
|
973
|
+
fiber = undefined;
|
|
974
|
+
if (current !== undefined) await current.dispose();
|
|
975
|
+
};
|
|
976
|
+
const setEnabled = async (enabled) => {
|
|
977
|
+
if (enabled === true && !ctx.get('tools')) {
|
|
978
|
+
throw new Error('the tools service is unavailable in this profile, so agent tools cannot be enabled');
|
|
979
|
+
}
|
|
980
|
+
const next = { tools: enabled === true };
|
|
981
|
+
writeSettings(file, next);
|
|
982
|
+
settings = next;
|
|
983
|
+
if (settings.tools) start();
|
|
984
|
+
else await stop();
|
|
985
|
+
return status();
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
if (settings.tools) start();
|
|
989
|
+
return { status, setEnabled };
|
|
990
|
+
}
|
|
991
|
+
|
|
929
992
|
function sendJson(res, status, value) {
|
|
930
993
|
if (res.headersSent) return;
|
|
931
994
|
res.statusCode = status;
|
|
@@ -970,7 +1033,7 @@ function rejectUnauthenticated(ctx, req, res) {
|
|
|
970
1033
|
return true;
|
|
971
1034
|
}
|
|
972
1035
|
|
|
973
|
-
async function handleApi(ctx, req, res, service) {
|
|
1036
|
+
async function handleApi(ctx, req, res, service, agentTools) {
|
|
974
1037
|
if (rejectUnauthenticated(ctx, req, res)) return;
|
|
975
1038
|
if (req.method === 'OPTIONS') {
|
|
976
1039
|
res.statusCode = 204;
|
|
@@ -1001,6 +1064,16 @@ async function handleApi(ctx, req, res, service) {
|
|
|
1001
1064
|
sendJson(res, 200, await service.compare(body.a, body.b));
|
|
1002
1065
|
return;
|
|
1003
1066
|
}
|
|
1067
|
+
if (req.method === 'GET' && pathname === '/settings') {
|
|
1068
|
+
sendJson(res, 200, { tools: agentTools.status() });
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (req.method === 'POST' && pathname === '/settings/tools') {
|
|
1072
|
+
const body = await readRequestBody(req);
|
|
1073
|
+
if (typeof body.enabled !== 'boolean') throw new Error('enabled must be a boolean');
|
|
1074
|
+
sendJson(res, 200, { tools: await agentTools.setEnabled(body.enabled) });
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1004
1077
|
const match = pathname.match(/^\/checkpoints\/([^/]+)$/);
|
|
1005
1078
|
if (req.method === 'GET' && match) {
|
|
1006
1079
|
sendJson(res, 200, await service.getCheckpoint(decodeURIComponent(match[1])));
|
|
@@ -1025,14 +1098,12 @@ export function apply(ctx, config = {}) {
|
|
|
1025
1098
|
const lifetime = new AbortController();
|
|
1026
1099
|
ctx.effect(() => () => { lifetime.abort(); }, 'llm-fingerprint: abort running captures');
|
|
1027
1100
|
const service = createService(ctx, root, lifetime.signal);
|
|
1028
|
-
|
|
1029
|
-
ctx.effect(() => ctx.tools.register(definition), `llm-fingerprint: ${definition.name}`);
|
|
1030
|
-
}
|
|
1101
|
+
const agentTools = createAgentTools(ctx, service, root);
|
|
1031
1102
|
ctx.effect(
|
|
1032
1103
|
() => ctx.webServer.register({
|
|
1033
1104
|
kind: 'prefix',
|
|
1034
1105
|
path: API_PREFIX,
|
|
1035
|
-
handler: (req, res) => handleApi(ctx, req, res, service),
|
|
1106
|
+
handler: (req, res) => handleApi(ctx, req, res, service, agentTools),
|
|
1036
1107
|
}),
|
|
1037
1108
|
'llm-fingerprint: http api',
|
|
1038
1109
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fingerprint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paper-protocol fingerprint records and comparison panel for configured DSH provider/model routes.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
42
|
"check": "node --check index.js && node --check core.js && node --check client.js",
|
|
43
|
-
"test": "node --test test/core.test.js"
|
|
43
|
+
"test": "node --test test/core.test.js test/plugin.test.js"
|
|
44
44
|
},
|
|
45
45
|
"dsh": {
|
|
46
46
|
"bundle": {
|