dsh-fingerprint 0.3.1 → 0.5.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 +14 -22
- package/client.js +249 -10
- package/docs/DESIGN.md +29 -16
- package/docs/screenshot.png +0 -0
- package/index.js +100 -9
- package/package.json +3 -3
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
|
|
|
@@ -48,24 +50,13 @@ dsh plugin --profile web add github:Jerry2262/dsh-fingerprint
|
|
|
48
50
|
## 使用
|
|
49
51
|
|
|
50
52
|
1. 选择服务商和模型。不能关闭 reasoning 的模型会置灰、不可选,并标注原因。
|
|
51
|
-
2.
|
|
52
|
-
3.
|
|
53
|
-
4.
|
|
54
|
-
5.
|
|
55
|
-
6.
|
|
56
|
-
|
|
57
|
-
|
|
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 服务,不维护第二套逻辑。
|
|
53
|
+
2. 点“检测可行性”。它用 2 个 cell × 3 次(6 个请求)实际发一次,确认这条路由能给出单词答案且不带 reasoning trace;通过后“开始采集”才可用,换服务商或模型会要求重新检测。模型元数据说支持 `off` 不代表服务商真的照做,这一步是唯一能提前发现的地方。
|
|
54
|
+
3. 设置每个 cell 的重复次数(默认 30,UI 最低 10),点“开始采集”。
|
|
55
|
+
4. 运行中的记录显示进度;完成后显示有效率、分半 JSD、纳入 cell 数。插件被卸载或进程重启时,未完成的记录会标为失败(保留已抓到的响应),可以直接删除,不会自动续跑。
|
|
56
|
+
5. 在“对比记录”里选两条记录,点“对比记录”。比较方式只有一种,结果会标注两边是否同一目标。
|
|
57
|
+
6. 结果包含差异分数、判定线、结论、关系、cell 级差异和警告。低于判定线只说明差异小,不等于 100% 是同一个模型。
|
|
58
|
+
7. 行内垃圾桶可以删除记录;删除前会二次确认。运行中的记录不能删除。
|
|
59
|
+
8. 面板底部的“设置”里有 Agent 工具开关,默认关闭。开启后把 6 个 `llm_fingerprint_*` 工具暴露给 agent(每个请求多约 4.3 KB 工具 schema),关闭后立即从后续请求里移除;选择保存在数据目录的 `settings.json`,删掉这个文件即回到默认关闭。
|
|
69
60
|
|
|
70
61
|
## 工作原理
|
|
71
62
|
|
|
@@ -79,6 +70,7 @@ UI 和工具调用同一个 Host 服务,不维护第二套逻辑。
|
|
|
79
70
|
|
|
80
71
|
- 插件不保存 API key;key 留在 DSH 的 provider/credential 层。
|
|
81
72
|
- 原始响应和指纹默认写到 `$DSH_HOME/llm-fingerprint/`,也可以由插件 row 的 `dataDir` 覆盖。
|
|
73
|
+
- 面板设置(目前只有 Agent 工具开关)存在同一数据目录的 `settings.json`。
|
|
82
74
|
- 删除记录会删除该目录下的 `meta.json`、`responses.jsonl` 和 `fingerprint.json`。
|
|
83
75
|
- 无法关闭 reasoning 的模型不会被静默采集。
|
|
84
76
|
|
|
@@ -93,14 +85,14 @@ UI 和工具调用同一个 Host 服务,不维护第二套逻辑。
|
|
|
93
85
|
|
|
94
86
|
## 开发
|
|
95
87
|
|
|
96
|
-
- `index.js`:Host plugin,`llm` / `webServer` service、
|
|
88
|
+
- `index.js`:Host plugin,`llm` / `webServer` service、HTTP API;面板开关开启时另注册 6 个 agent 工具。
|
|
97
89
|
- `core.js`:协议常量、提示词 battery、归一化、JSD、指纹与对比。
|
|
98
90
|
- `client.js`:Web 面板(`main` + `sidebar.panellist`)、详情抽屉、对比与删除确认。
|
|
99
91
|
- `prompts.json`:论文 paper-1 的 10 个任务 × 4 种语言。
|
|
100
92
|
- `locale/`:插件元数据和 UI 文案。
|
|
101
93
|
- `docs/DESIGN.md`:设计细节。
|
|
102
94
|
- `npm run check`:JS 语法检查(无构建步骤)。
|
|
103
|
-
- `npm test
|
|
95
|
+
- `npm test`:核心与插件注册回归测试(判定线、覆盖门槛、协议拒绝、重复 cell、主要答案,以及 agent 工具默认关闭/开关注册)。
|
|
104
96
|
|
|
105
97
|
## 许可与来源
|
|
106
98
|
|
package/client.js
CHANGED
|
@@ -64,6 +64,12 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
noCompatibleModels: 'This provider has no model that can disable reasoning.',
|
|
65
65
|
noCompatibleModelsShort: 'No models eligible for capture',
|
|
66
66
|
offAvailable: 'Reasoning will be disabled (off) for capture.',
|
|
67
|
+
checkTitle: 'Check feasibility',
|
|
68
|
+
checking: 'Checking…',
|
|
69
|
+
checkHint: 'Sends 2 cells × 3 samples (6 requests) to confirm this route answers in one word without a reasoning trace.',
|
|
70
|
+
checkRequired: 'Run the feasibility check first: it confirms the route satisfies the protocol in 6 requests instead of discovering it after the full battery.',
|
|
71
|
+
checkPassed: 'Feasibility check passed: {valid}/{responses} one-word answers, {leak} with a reasoning trace.',
|
|
72
|
+
checkFailed: 'Feasibility check failed: {valid}/{responses} one-word answers, {leak} with a reasoning trace, {errors} failed requests.',
|
|
67
73
|
effort: 'Reasoning effort',
|
|
68
74
|
started: 'Capture started:',
|
|
69
75
|
error: 'Error',
|
|
@@ -120,6 +126,11 @@ window.__ModuleLoader__.load({
|
|
|
120
126
|
referenceValidity: 'valid rate 97.6% overall, per-model median 99.6%',
|
|
121
127
|
referenceSplitHalf: 'split-half JSD 0.140 for the same model, 0.463 for different models',
|
|
122
128
|
referenceScore: 'The cutoff is 0.280, the 1st percentile of different-model distances: in 100 different models, about 1 would still look the same.',
|
|
129
|
+
settingsTitle: 'Settings',
|
|
130
|
+
toolsTitle: 'Agent tools',
|
|
131
|
+
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.',
|
|
132
|
+
toolsUnavailable: 'This profile does not provide the tools service, so the switch cannot be turned on.',
|
|
133
|
+
toolsUpdated: 'Agent tool visibility updated.',
|
|
123
134
|
};
|
|
124
135
|
|
|
125
136
|
const zh = {
|
|
@@ -178,6 +189,12 @@ window.__ModuleLoader__.load({
|
|
|
178
189
|
noCompatibleModels: '该服务商没有支持关闭推理的模型,不能用于论文协议。',
|
|
179
190
|
noCompatibleModelsShort: '无符合采集条件的模型',
|
|
180
191
|
offAvailable: '采集时将关闭推理(off)。',
|
|
192
|
+
checkTitle: '检测可行性',
|
|
193
|
+
checking: '检测中…',
|
|
194
|
+
checkHint: '先用 2 个 cell × 3 次(6 个请求)确认这条路由能给出单词答案,且不带 reasoning trace。',
|
|
195
|
+
checkRequired: '请先检测可行性:用 6 个请求确认这条路由满足论文协议,避免跑完 1200 个请求才发现结果无效。',
|
|
196
|
+
checkPassed: '可行性检测通过:{valid}/{responses} 个单词答案,{leak} 个带 reasoning trace。',
|
|
197
|
+
checkFailed: '可行性检测未通过:{valid}/{responses} 个单词答案,{leak} 个带 reasoning trace,{errors} 个请求失败。',
|
|
181
198
|
effort: '推理档位',
|
|
182
199
|
started: '指纹采集已开始:',
|
|
183
200
|
error: '错误',
|
|
@@ -234,6 +251,11 @@ window.__ModuleLoader__.load({
|
|
|
234
251
|
referenceValidity: '有效率总体 97.6%,模型内中位 99.6%',
|
|
235
252
|
referenceSplitHalf: '同一模型的分半差异通常 0.140,不同模型通常 0.463',
|
|
236
253
|
referenceScore: '判定线是 0.280,取自不同模型距离的 1% 分位:100 个不同模型里,大约只有 1 个会被看成相同。',
|
|
254
|
+
settingsTitle: '设置',
|
|
255
|
+
toolsTitle: 'Agent 工具',
|
|
256
|
+
toolsHint: '把 6 个采集、对比工具暴露给 agent。每个请求会多约 4.3 KB 工具 schema,适合调试或自动化。',
|
|
257
|
+
toolsUnavailable: '当前 profile 没有 tools 服务,无法开启这个开关。',
|
|
258
|
+
toolsUpdated: 'agent 工具开关已更新。',
|
|
237
259
|
};
|
|
238
260
|
|
|
239
261
|
async function api(path, init) {
|
|
@@ -299,6 +321,16 @@ window.__ModuleLoader__.load({
|
|
|
299
321
|
return text;
|
|
300
322
|
}
|
|
301
323
|
|
|
324
|
+
/** One line describing the last feasibility check for the selected target. */
|
|
325
|
+
function checkMessage(check, passed, t) {
|
|
326
|
+
const fill = (text) => text
|
|
327
|
+
.replace('{valid}', String(check.valid))
|
|
328
|
+
.replace('{responses}', String(check.responses))
|
|
329
|
+
.replace('{leak}', String(check.reasoningLeak))
|
|
330
|
+
.replace('{errors}', String(check.errors));
|
|
331
|
+
return check.error ? `${fill(t('checkFailed'))} ${check.error}` : fill(t(passed ? 'checkPassed' : 'checkFailed'));
|
|
332
|
+
}
|
|
333
|
+
|
|
302
334
|
function friendlyError(message, t) {
|
|
303
335
|
const text = String(message ?? '');
|
|
304
336
|
if (/cannot disable reasoning/i.test(text)) return t('errorNoReasoning');
|
|
@@ -307,6 +339,7 @@ window.__ModuleLoader__.load({
|
|
|
307
339
|
if (/still running/i.test(text)) return t('errorStillRunning');
|
|
308
340
|
if (/unknown fingerprint record/i.test(text)) return t('errorRecordNotFound');
|
|
309
341
|
if (/no completed fingerprint|two record ids|record id is required/i.test(text)) return t('errorRecordNeedsFingerprint');
|
|
342
|
+
if (/tools service is unavailable/i.test(text)) return t('toolsUnavailable');
|
|
310
343
|
return text;
|
|
311
344
|
}
|
|
312
345
|
|
|
@@ -336,7 +369,7 @@ window.__ModuleLoader__.load({
|
|
|
336
369
|
function visibleWarnings(comparison) {
|
|
337
370
|
return (comparison.warnings || []).filter((item) => {
|
|
338
371
|
if (comparison.verdictReason === 'partial_battery' && /full battery/i.test(item)) return false;
|
|
339
|
-
if (comparison.verdictReason === 'repetitions' && /
|
|
372
|
+
if (comparison.verdictReason === 'repetitions' && /cutoff needs 30/i.test(item)) return false;
|
|
340
373
|
return true;
|
|
341
374
|
});
|
|
342
375
|
}
|
|
@@ -444,6 +477,39 @@ window.__ModuleLoader__.load({
|
|
|
444
477
|
fontSize: '11px',
|
|
445
478
|
cursor: 'pointer',
|
|
446
479
|
},
|
|
480
|
+
settingRow: {
|
|
481
|
+
display: 'flex',
|
|
482
|
+
alignItems: 'center',
|
|
483
|
+
justifyContent: 'space-between',
|
|
484
|
+
gap: '20px',
|
|
485
|
+
},
|
|
486
|
+
switch: {
|
|
487
|
+
position: 'relative',
|
|
488
|
+
flex: 'none',
|
|
489
|
+
width: '38px',
|
|
490
|
+
height: '22px',
|
|
491
|
+
padding: 0,
|
|
492
|
+
borderRadius: '999px',
|
|
493
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
494
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
495
|
+
cursor: 'pointer',
|
|
496
|
+
transition: 'background .15s ease, border-color .15s ease',
|
|
497
|
+
},
|
|
498
|
+
switchOn: {
|
|
499
|
+
background: 'var(--dsw-alias-brand-primary)',
|
|
500
|
+
borderColor: 'var(--dsw-alias-brand-primary)',
|
|
501
|
+
},
|
|
502
|
+
switchKnob: {
|
|
503
|
+
position: 'absolute',
|
|
504
|
+
top: '2px',
|
|
505
|
+
left: '2px',
|
|
506
|
+
width: '16px',
|
|
507
|
+
height: '16px',
|
|
508
|
+
borderRadius: '50%',
|
|
509
|
+
background: '#fff',
|
|
510
|
+
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.3)',
|
|
511
|
+
transition: 'transform .15s ease',
|
|
512
|
+
},
|
|
447
513
|
iconButton: {
|
|
448
514
|
display: 'inline-flex',
|
|
449
515
|
alignItems: 'center',
|
|
@@ -619,6 +685,48 @@ window.__ModuleLoader__.load({
|
|
|
619
685
|
);
|
|
620
686
|
}
|
|
621
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Sidebar glyph: the fingerprint mark drawn to the same optical spec as the
|
|
690
|
+
* built-in panel icons — 16 viewBox, 1.33 stroke, currentColor, artwork in
|
|
691
|
+
* the 1.3–14.7 box — so it matches their visual size. The sidebar row owns
|
|
692
|
+
* the edge and passes it as `size`.
|
|
693
|
+
*/
|
|
694
|
+
function PanelIcon({ size }) {
|
|
695
|
+
return h('svg', {
|
|
696
|
+
viewBox: '0 0 16 16',
|
|
697
|
+
width: size,
|
|
698
|
+
height: size,
|
|
699
|
+
fill: 'none',
|
|
700
|
+
'aria-hidden': true,
|
|
701
|
+
strokeWidth: 1.33,
|
|
702
|
+
style: { display: 'block', flex: 'none' },
|
|
703
|
+
},
|
|
704
|
+
h('rect', {
|
|
705
|
+
x: 1.97,
|
|
706
|
+
y: 1.97,
|
|
707
|
+
width: 12.06,
|
|
708
|
+
height: 12.06,
|
|
709
|
+
rx: 3.1,
|
|
710
|
+
stroke: 'currentColor',
|
|
711
|
+
}),
|
|
712
|
+
h('path', {
|
|
713
|
+
d: 'M4.84 9.57c0-1.83 1.15-3.15 3.16-3.15s3.16 1.32 3.16 3.15',
|
|
714
|
+
stroke: 'currentColor',
|
|
715
|
+
strokeLinecap: 'round',
|
|
716
|
+
}),
|
|
717
|
+
h('path', {
|
|
718
|
+
d: 'M6.27 10.35c0-1.05.58-1.83 1.73-1.83s1.73.78 1.73 1.83',
|
|
719
|
+
stroke: 'currentColor',
|
|
720
|
+
strokeLinecap: 'round',
|
|
721
|
+
}),
|
|
722
|
+
h('path', {
|
|
723
|
+
d: 'M8 11.4v-1.3',
|
|
724
|
+
stroke: 'currentColor',
|
|
725
|
+
strokeLinecap: 'round',
|
|
726
|
+
}),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
622
730
|
function TrashIcon({ size = 14 }) {
|
|
623
731
|
return h('svg', {
|
|
624
732
|
viewBox: '0 0 24 24',
|
|
@@ -693,6 +801,8 @@ window.__ModuleLoader__.load({
|
|
|
693
801
|
const [modelId, setModelId] = useState('');
|
|
694
802
|
const [reps, setReps] = useState(30);
|
|
695
803
|
const [starting, setStarting] = useState(false);
|
|
804
|
+
const [checking, setChecking] = useState(false);
|
|
805
|
+
const [check, setCheck] = useState(null);
|
|
696
806
|
const [comparing, setComparing] = useState(false);
|
|
697
807
|
const [deleting, setDeleting] = useState(false);
|
|
698
808
|
const [pendingDelete, setPendingDelete] = useState(null);
|
|
@@ -707,6 +817,8 @@ window.__ModuleLoader__.load({
|
|
|
707
817
|
const [detail, setDetail] = useState(null);
|
|
708
818
|
const [detailId, setDetailId] = useState('');
|
|
709
819
|
const [compact, setCompact] = useState(false);
|
|
820
|
+
const [toolsSetting, setToolsSetting] = useState(null);
|
|
821
|
+
const [savingTools, setSavingTools] = useState(false);
|
|
710
822
|
const resultRef = useRef(null);
|
|
711
823
|
const pageRef = useRef(null);
|
|
712
824
|
|
|
@@ -738,8 +850,13 @@ window.__ModuleLoader__.load({
|
|
|
738
850
|
let cancelled = false;
|
|
739
851
|
(async () => {
|
|
740
852
|
try {
|
|
741
|
-
const [targetData, checkpointData] = await Promise.all([
|
|
853
|
+
const [targetData, checkpointData, settingsData] = await Promise.all([
|
|
854
|
+
api('/targets'),
|
|
855
|
+
api('/checkpoints'),
|
|
856
|
+
api('/settings').catch(() => null),
|
|
857
|
+
]);
|
|
742
858
|
if (cancelled) return;
|
|
859
|
+
if (settingsData) setToolsSetting(settingsData.tools || null);
|
|
743
860
|
const providers = (targetData.providers || []).filter((entry) => (entry.models || []).length > 0);
|
|
744
861
|
setTargets(providers);
|
|
745
862
|
setCheckpoints(checkpointData.checkpoints || []);
|
|
@@ -788,6 +905,9 @@ window.__ModuleLoader__.load({
|
|
|
788
905
|
const hasIncompatible = models.some((entry) => entry.paperCompatible === false);
|
|
789
906
|
const noCompatibleModels = models.length > 0 && compatibleModels.length === 0;
|
|
790
907
|
const modelCompatible = model ? model.paperCompatible !== false : false;
|
|
908
|
+
/** A feasibility check only counts for the target it ran against. */
|
|
909
|
+
const checkMatches = Boolean(check && check.provider === providerId && check.model === modelId);
|
|
910
|
+
const checkPassed = checkMatches && check.passed === true;
|
|
791
911
|
const completed = checkpoints.filter((entry) => entry.status === 'completed' || entry.status === 'completed_with_failures');
|
|
792
912
|
const bOptions = completed.filter((entry) => entry.id !== a);
|
|
793
913
|
|
|
@@ -799,10 +919,54 @@ window.__ModuleLoader__.load({
|
|
|
799
919
|
setModelId(nextModel ? nextModel.id : '');
|
|
800
920
|
setWarning('');
|
|
801
921
|
setNotice('');
|
|
922
|
+
setCheck(null);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function runCheck() {
|
|
926
|
+
if (!providerId || !modelId || !modelCompatible) return;
|
|
927
|
+
setChecking(true);
|
|
928
|
+
setError('');
|
|
929
|
+
setWarning('');
|
|
930
|
+
setNotice('');
|
|
931
|
+
try {
|
|
932
|
+
const result = await api('/probe', {
|
|
933
|
+
method: 'POST',
|
|
934
|
+
body: JSON.stringify({ provider: providerId, model: modelId }),
|
|
935
|
+
});
|
|
936
|
+
const summary = result.summary || {};
|
|
937
|
+
setCheck({
|
|
938
|
+
provider: providerId,
|
|
939
|
+
model: modelId,
|
|
940
|
+
passed: summary.protocolCompliant === true,
|
|
941
|
+
responses: Number(summary.responses ?? 0),
|
|
942
|
+
valid: Number(summary.valid ?? 0),
|
|
943
|
+
reasoningLeak: Number(summary.reasoningLeak ?? 0),
|
|
944
|
+
errors: Number(summary.errors ?? 0),
|
|
945
|
+
probeId: result.probeId ?? null,
|
|
946
|
+
});
|
|
947
|
+
} catch (cause) {
|
|
948
|
+
setCheck({
|
|
949
|
+
provider: providerId,
|
|
950
|
+
model: modelId,
|
|
951
|
+
passed: false,
|
|
952
|
+
responses: 0,
|
|
953
|
+
valid: 0,
|
|
954
|
+
reasoningLeak: 0,
|
|
955
|
+
errors: 1,
|
|
956
|
+
error: friendlyError(cause.message || cause, t),
|
|
957
|
+
probeId: null,
|
|
958
|
+
});
|
|
959
|
+
} finally {
|
|
960
|
+
setChecking(false);
|
|
961
|
+
}
|
|
802
962
|
}
|
|
803
963
|
|
|
804
964
|
async function start() {
|
|
805
965
|
if (!providerId || !modelId) return;
|
|
966
|
+
if (!checkPassed) {
|
|
967
|
+
setError(t('checkRequired'));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
806
970
|
if (!modelCompatible) {
|
|
807
971
|
setError(noCompatibleModels ? t('noCompatibleModels') : t('noOff'));
|
|
808
972
|
return;
|
|
@@ -814,7 +978,12 @@ window.__ModuleLoader__.load({
|
|
|
814
978
|
try {
|
|
815
979
|
const created = await api('/checkpoints', {
|
|
816
980
|
method: 'POST',
|
|
817
|
-
body: JSON.stringify({
|
|
981
|
+
body: JSON.stringify({
|
|
982
|
+
provider: providerId,
|
|
983
|
+
model: modelId,
|
|
984
|
+
reps: Number(reps),
|
|
985
|
+
...(checkPassed && check.probeId ? { probeId: check.probeId } : {}),
|
|
986
|
+
}),
|
|
818
987
|
});
|
|
819
988
|
const recordId = created.recordId ?? created.checkpointId;
|
|
820
989
|
setNotice(`${t('started')} ${recordId}`);
|
|
@@ -894,6 +1063,25 @@ window.__ModuleLoader__.load({
|
|
|
894
1063
|
}
|
|
895
1064
|
}
|
|
896
1065
|
|
|
1066
|
+
async function toggleTools() {
|
|
1067
|
+
if (!toolsSetting || savingTools) return;
|
|
1068
|
+
setSavingTools(true);
|
|
1069
|
+
setError('');
|
|
1070
|
+
setNotice('');
|
|
1071
|
+
try {
|
|
1072
|
+
const data = await api('/settings/tools', {
|
|
1073
|
+
method: 'POST',
|
|
1074
|
+
body: JSON.stringify({ enabled: !toolsSetting.enabled }),
|
|
1075
|
+
});
|
|
1076
|
+
setToolsSetting(data.tools);
|
|
1077
|
+
setNotice(t('toolsUpdated'));
|
|
1078
|
+
} catch (cause) {
|
|
1079
|
+
setError(friendlyError(cause.message || cause, t));
|
|
1080
|
+
} finally {
|
|
1081
|
+
setSavingTools(false);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
897
1085
|
const estimate = 40 * Math.max(10, Math.min(100, Number(reps) || 30));
|
|
898
1086
|
|
|
899
1087
|
return h('div', { style: S.page },
|
|
@@ -934,7 +1122,10 @@ window.__ModuleLoader__.load({
|
|
|
934
1122
|
h('select', {
|
|
935
1123
|
style: S.input,
|
|
936
1124
|
value: modelId,
|
|
937
|
-
onChange: (event) =>
|
|
1125
|
+
onChange: (event) => {
|
|
1126
|
+
setModelId(event.target.value);
|
|
1127
|
+
setCheck(null);
|
|
1128
|
+
},
|
|
938
1129
|
disabled: models.length === 0 || noCompatibleModels,
|
|
939
1130
|
},
|
|
940
1131
|
h('option', { value: '' }, models.length === 0 ? t('noModels') : noCompatibleModels ? t('noCompatibleModelsShort') : '—'),
|
|
@@ -958,11 +1149,19 @@ window.__ModuleLoader__.load({
|
|
|
958
1149
|
onChange: (event) => setReps(Math.max(10, Math.min(100, Number(event.target.value) || 30))),
|
|
959
1150
|
}),
|
|
960
1151
|
),
|
|
961
|
-
h('
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1152
|
+
h('div', { style: { display: 'flex', gap: '8px', flex: '1 1 auto', justifyContent: 'flex-end' } },
|
|
1153
|
+
h('button', {
|
|
1154
|
+
style: { ...S.buttonSecondary, ...(checking || !modelCompatible || !providerId || !modelId ? S.disabled : {}) },
|
|
1155
|
+
disabled: checking || starting || !providerId || !modelId || !modelCompatible,
|
|
1156
|
+
onClick: runCheck,
|
|
1157
|
+
}, checking ? t('checking') : t('checkTitle')),
|
|
1158
|
+
h('button', {
|
|
1159
|
+
style: { ...S.button, ...(starting || !modelCompatible || !checkPassed ? S.disabled : {}) },
|
|
1160
|
+
disabled: starting || !providerId || !modelId || !modelCompatible || !checkPassed,
|
|
1161
|
+
title: checkPassed ? undefined : t('checkRequired'),
|
|
1162
|
+
onClick: start,
|
|
1163
|
+
}, starting ? t('starting') : t('start')),
|
|
1164
|
+
),
|
|
966
1165
|
),
|
|
967
1166
|
h('div', { style: { ...S.hint, marginTop: '10px' } },
|
|
968
1167
|
`${t('estimated')}: ${estimate} ${t('requests')} (40 ${t('cellsUnit')} × ${reps})`,
|
|
@@ -982,6 +1181,16 @@ window.__ModuleLoader__.load({
|
|
|
982
1181
|
? h('div', { style: { ...S.hint, color: 'var(--dsw-alias-state-warn-primary)' } }, t('noCompatibleModels'))
|
|
983
1182
|
: null,
|
|
984
1183
|
hasIncompatible ? h('div', { style: S.hint }, t('incompatibleHint')) : null,
|
|
1184
|
+
model
|
|
1185
|
+
? h('div', {
|
|
1186
|
+
style: {
|
|
1187
|
+
...S.hint,
|
|
1188
|
+
color: checkMatches
|
|
1189
|
+
? (checkPassed ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)')
|
|
1190
|
+
: 'var(--dsw-alias-label-secondary)',
|
|
1191
|
+
},
|
|
1192
|
+
}, checkMatches ? checkMessage(check, checkPassed, t) : t('checkHint'))
|
|
1193
|
+
: null,
|
|
985
1194
|
),
|
|
986
1195
|
|
|
987
1196
|
h(Section, {
|
|
@@ -1144,6 +1353,36 @@ window.__ModuleLoader__.load({
|
|
|
1144
1353
|
: null,
|
|
1145
1354
|
) : null,
|
|
1146
1355
|
),
|
|
1356
|
+
|
|
1357
|
+
h(Section, { title: t('settingsTitle') },
|
|
1358
|
+
h('div', { style: S.settingRow },
|
|
1359
|
+
h('div', { style: { minWidth: 0 } },
|
|
1360
|
+
h('div', { style: { fontSize: '13px', fontWeight: 600 } }, t('toolsTitle')),
|
|
1361
|
+
h('p', { style: { ...S.hint, margin: '4px 0 0' } }, t('toolsHint')),
|
|
1362
|
+
toolsSetting && !toolsSetting.available
|
|
1363
|
+
? h('p', { style: { ...S.hint, margin: '4px 0 0', color: 'var(--dsw-alias-state-warn-primary)' } }, t('toolsUnavailable'))
|
|
1364
|
+
: null,
|
|
1365
|
+
),
|
|
1366
|
+
h('button', {
|
|
1367
|
+
type: 'button',
|
|
1368
|
+
role: 'switch',
|
|
1369
|
+
'aria-checked': Boolean(toolsSetting && toolsSetting.enabled),
|
|
1370
|
+
'aria-label': t('toolsTitle'),
|
|
1371
|
+
disabled: !toolsSetting || savingTools || !toolsSetting.available,
|
|
1372
|
+
style: {
|
|
1373
|
+
...S.switch,
|
|
1374
|
+
...(toolsSetting && toolsSetting.enabled ? S.switchOn : {}),
|
|
1375
|
+
...(!toolsSetting || savingTools || !toolsSetting.available ? S.disabled : {}),
|
|
1376
|
+
},
|
|
1377
|
+
onClick: toggleTools,
|
|
1378
|
+
}, h('span', {
|
|
1379
|
+
style: {
|
|
1380
|
+
...S.switchKnob,
|
|
1381
|
+
transform: toolsSetting && toolsSetting.enabled ? 'translateX(16px)' : 'none',
|
|
1382
|
+
},
|
|
1383
|
+
})),
|
|
1384
|
+
),
|
|
1385
|
+
),
|
|
1147
1386
|
),
|
|
1148
1387
|
detail ? h('div', {
|
|
1149
1388
|
style: S.drawerOverlay,
|
|
@@ -1276,7 +1515,7 @@ window.__ModuleLoader__.load({
|
|
|
1276
1515
|
order: 30,
|
|
1277
1516
|
locale: NS,
|
|
1278
1517
|
label: () => t('title'),
|
|
1279
|
-
},
|
|
1518
|
+
}, PanelIcon));
|
|
1280
1519
|
},
|
|
1281
1520
|
};
|
|
1282
1521
|
},
|
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
|
|
|
@@ -75,6 +78,14 @@ llm-fingerprint/
|
|
|
75
78
|
|
|
76
79
|
遇到 `MissingSessionID` 时停止调度后续采样,等待已经发出的并发请求结束后将记录标为失败,保留原始错误,不继续发完整批次。
|
|
77
80
|
|
|
81
|
+
### 采集前的可行性检测
|
|
82
|
+
|
|
83
|
+
面板选完服务商和模型后必须先点“检测可行性”:它调用 `POST /probe`(2 个 cell × 3 次,共 6 个请求),只有返回的 `summary.protocolCompliant` 为真时“开始采集”才可用;换服务商或模型会清掉结果,要求重新检测。
|
|
84
|
+
|
|
85
|
+
原因是元数据里的 `off` 只说明适配器愿意请求关闭档位,不保证服务商真的照做。路由把 `off` 翻成“不发送 reasoning 参数”时,服务商可能仍按默认推理,`max_tokens=16` 全被推理吃光,整批响应都会变成 `post_reasoning`:`command-goat` 的 `openai-completions` 路由就这样跑完 1200 个请求才暴露 0 有效。这种失败只有在真的发一次请求后才看得见,所以检测放在采集之前,用 6 个请求代替 1200 个。
|
|
86
|
+
|
|
87
|
+
检测只拦面板入口:HTTP API 和 agent 工具仍然可以直接 `POST /checkpoints`(`startCheckpoint` 只做元数据层面的 `supportsPaperProtocol` 检查),这条路径留给诊断和自动化。通过检测后再采集时,面板把 `probeId` 一并提交,记录到 `meta.probeId`,事后可以把记录和当次检测对上。检测日志按时间保留最近 `PROBE_LOG_RETENTION`(20)条,其余在写入时删除。
|
|
88
|
+
|
|
78
89
|
## 5. 兼容性硬拦截
|
|
79
90
|
|
|
80
91
|
`supportsPaperProtocol(info)` 的规则:
|
|
@@ -87,7 +98,7 @@ llm-fingerprint/
|
|
|
87
98
|
|
|
88
99
|
- UI 模型下拉里仍然列出,但 `disabled`,文本标注“不可关闭推理”;
|
|
89
100
|
- 服务商下所有模型都不可用时,服务商选项标注“无可用模型”,模型选择禁用,Start 禁用,并显示原因;
|
|
90
|
-
- Host `startCheckpoint` 再检查一次,API 和 agent 工具都无法绕过;
|
|
101
|
+
- Host `startCheckpoint` 再检查一次,HTTP API 和 agent 工具都无法绕过;
|
|
91
102
|
- `probe` 仍然可以用于诊断,但不会创建记录。
|
|
92
103
|
|
|
93
104
|
这套判断完全由运行时配置/适配器元数据计算,不维护模型名黑名单。给某个模型在 DSH 配置里加上 `off` 映射后,它会自动变为可用,不需要改插件代码。
|
|
@@ -124,11 +135,12 @@ llm-fingerprint/
|
|
|
124
135
|
|
|
125
136
|
## 8. 限制与不做的事
|
|
126
137
|
|
|
127
|
-
- 兼容范围:`engines.dsh` 限定 DSH 0.2.1 系列(`>=0.2.1-rc.2 <0.2.2-0
|
|
138
|
+
- 兼容范围:`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
139
|
- 不做 post-reasoning 指纹模式;只能 reasoning 的模型被拒绝。
|
|
129
140
|
- 不读取/保存 provider key;认证由 DSH 适配器负责。
|
|
130
141
|
- 不读取 provider 返回的 model 字符串或聚合器上游路由;`ctx.llm.stream` 不暴露这些。
|
|
131
142
|
- 不把“分布不一致”解释成“供应商故意掺水”;它只是可观察行为差异。
|
|
143
|
+
- agent 工具默认关闭,由面板“设置”里的开关控制;开启后 6 个工具定义会进入每个请求的上下文(实测约 4.3 KB / 1.2K tokens),这是调试或自动化的成本,不是面板的依赖。
|
|
132
144
|
- 不实现自动定时巡检、多端点距离矩阵、CSV 导出、自定义 battery 编辑器;这些是后续功能。
|
|
133
145
|
|
|
134
146
|
## 9. 维护
|
|
@@ -136,4 +148,5 @@ llm-fingerprint/
|
|
|
136
148
|
- 提示词版本写在 `prompts.json` 的 `version` 和每条记录的 `promptVersion`;改提示词需要重新采集,旧记录不能直接和新记录混比。
|
|
137
149
|
- 判定线常量在 `core.js` 的 `CUTOFF`;修改时需要同步 README/DESIGN 并说明校准来源。
|
|
138
150
|
- 数据目录 append-only;分析产物可以删除后从 `responses.jsonl` 重新计算。
|
|
139
|
-
-
|
|
151
|
+
- 面板设置只有 agent 工具开关,写在数据目录的 `settings.json`;删除该文件即恢复默认关闭,不影响记录数据。
|
|
152
|
+
- 代码无构建步骤;`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;
|
|
@@ -46,6 +47,8 @@ const MAX_TOKENS = 16;
|
|
|
46
47
|
const TEMPERATURE = 1.0;
|
|
47
48
|
const DEFAULT_REPS = 30;
|
|
48
49
|
const API_PREFIX = '/llm-fingerprint/api';
|
|
50
|
+
/** Feasibility checks are cheap and disposable: keep the newest logs only. */
|
|
51
|
+
const PROBE_LOG_RETENTION = 20;
|
|
49
52
|
|
|
50
53
|
function resolveDataDir(config) {
|
|
51
54
|
const configured = config && typeof config.dataDir === 'string' && config.dataDir.trim();
|
|
@@ -475,6 +478,18 @@ function recoverInterruptedRecords(root) {
|
|
|
475
478
|
return recovered;
|
|
476
479
|
}
|
|
477
480
|
|
|
481
|
+
/** Keep the probe directory bounded: probe ids start with a timestamp, so name order is age order. */
|
|
482
|
+
function pruneProbeLogs(dir) {
|
|
483
|
+
const names = readdirSync(dir).filter((name) => /^probe-.+\.json$/.test(name)).sort();
|
|
484
|
+
for (const name of names.slice(0, Math.max(0, names.length - PROBE_LOG_RETENTION))) {
|
|
485
|
+
try {
|
|
486
|
+
rmSync(path.join(dir, name), { force: true });
|
|
487
|
+
} catch {
|
|
488
|
+
// A log that cannot be removed is not worth failing a diagnostic check over.
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
478
493
|
function checkpointSummary(meta) {
|
|
479
494
|
return {
|
|
480
495
|
id: meta.id,
|
|
@@ -649,6 +664,7 @@ function createService(ctx, root, lifetimeSignal) {
|
|
|
649
664
|
summary,
|
|
650
665
|
responses,
|
|
651
666
|
}, null, 2)}\n`);
|
|
667
|
+
pruneProbeLogs(probeDir);
|
|
652
668
|
return {
|
|
653
669
|
probeId,
|
|
654
670
|
provider: args.provider,
|
|
@@ -680,6 +696,10 @@ function createService(ctx, root, lifetimeSignal) {
|
|
|
680
696
|
if (!cellById(cellId)) throw new Error(`unknown cell "${cellId}"`);
|
|
681
697
|
}
|
|
682
698
|
const reps = Math.max(1, Math.min(100, Number(args.reps ?? DEFAULT_REPS)));
|
|
699
|
+
const probeId = args.probeId == null ? null : String(args.probeId);
|
|
700
|
+
if (probeId !== null && !/^probe-[0-9A-Za-z-]+$/.test(probeId)) {
|
|
701
|
+
throw new Error('invalid probe id');
|
|
702
|
+
}
|
|
683
703
|
const id = `cp-${nowUtc().replace(/[-:.]/g, '').slice(0, 15)}-${randomUUID().slice(0, 8)}`;
|
|
684
704
|
const provider = await providerInfo(ctx, args.provider);
|
|
685
705
|
const warnings = [];
|
|
@@ -701,6 +721,7 @@ function createService(ctx, root, lifetimeSignal) {
|
|
|
701
721
|
reasoningEffort: effort,
|
|
702
722
|
reasoningPolicy: effort == null ? 'none' : effortIsDisabled(effort) ? 'disabled' : 'least-available',
|
|
703
723
|
reasoningLeakResponses: 0,
|
|
724
|
+
...probeId === null ? {} : { probeId },
|
|
704
725
|
modelInfo: adapterMetadata(info),
|
|
705
726
|
request: { temperature: TEMPERATURE, maxTokens: MAX_TOKENS, systemPrompt: 'per-cell', sessionPolicy: 'new-per-sample-reuse-on-retry' },
|
|
706
727
|
usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0 },
|
|
@@ -926,6 +947,68 @@ function toolDefinitions(service) {
|
|
|
926
947
|
];
|
|
927
948
|
}
|
|
928
949
|
|
|
950
|
+
const AGENT_TOOLS_SETTINGS_FILE = 'settings.json';
|
|
951
|
+
|
|
952
|
+
/** Read the persisted panel preferences; unreadable or malformed files fall back to the defaults. */
|
|
953
|
+
function readSettings(file) {
|
|
954
|
+
try {
|
|
955
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
956
|
+
return { tools: value?.tools === true };
|
|
957
|
+
} catch {
|
|
958
|
+
return { tools: false };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** Persist the panel preferences. Write failures surface to the caller instead of being swallowed. */
|
|
963
|
+
function writeSettings(file, settings) {
|
|
964
|
+
writeFileSync(file, `${JSON.stringify({ tools: settings.tools === true }, null, 2)}\n`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Own the optional agent tool surface: the six tools are registered exactly
|
|
969
|
+
* while the Web panel's toggle is on.
|
|
970
|
+
*
|
|
971
|
+
* The setting persists in the plugin's data directory (`settings.json`) and
|
|
972
|
+
* defaults to off, so a fresh install adds no tool definitions to the model
|
|
973
|
+
* context. Registration waits for the `tools` service through `ctx.inject`;
|
|
974
|
+
* turning the toggle off disposes that child fiber, which unregisters the tools
|
|
975
|
+
* immediately — no profile reload or DSH restart involved.
|
|
976
|
+
*/
|
|
977
|
+
function createAgentTools(ctx, service, root) {
|
|
978
|
+
const file = path.join(root, AGENT_TOOLS_SETTINGS_FILE);
|
|
979
|
+
let settings = readSettings(file);
|
|
980
|
+
let fiber;
|
|
981
|
+
|
|
982
|
+
const status = () => ({ enabled: settings.tools, available: Boolean(ctx.get('tools')) });
|
|
983
|
+
const start = () => {
|
|
984
|
+
if (fiber !== undefined) return;
|
|
985
|
+
fiber = ctx.inject(['tools'], function registerFingerprintTools(toolCtx) {
|
|
986
|
+
for (const definition of toolDefinitions(service)) {
|
|
987
|
+
toolCtx.effect(() => toolCtx.tools.register(definition), `llm-fingerprint: ${definition.name}`);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
const stop = async () => {
|
|
992
|
+
const current = fiber;
|
|
993
|
+
fiber = undefined;
|
|
994
|
+
if (current !== undefined) await current.dispose();
|
|
995
|
+
};
|
|
996
|
+
const setEnabled = async (enabled) => {
|
|
997
|
+
if (enabled === true && !ctx.get('tools')) {
|
|
998
|
+
throw new Error('the tools service is unavailable in this profile, so agent tools cannot be enabled');
|
|
999
|
+
}
|
|
1000
|
+
const next = { tools: enabled === true };
|
|
1001
|
+
writeSettings(file, next);
|
|
1002
|
+
settings = next;
|
|
1003
|
+
if (settings.tools) start();
|
|
1004
|
+
else await stop();
|
|
1005
|
+
return status();
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
if (settings.tools) start();
|
|
1009
|
+
return { status, setEnabled };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
929
1012
|
function sendJson(res, status, value) {
|
|
930
1013
|
if (res.headersSent) return;
|
|
931
1014
|
res.statusCode = status;
|
|
@@ -970,7 +1053,7 @@ function rejectUnauthenticated(ctx, req, res) {
|
|
|
970
1053
|
return true;
|
|
971
1054
|
}
|
|
972
1055
|
|
|
973
|
-
async function handleApi(ctx, req, res, service) {
|
|
1056
|
+
async function handleApi(ctx, req, res, service, agentTools) {
|
|
974
1057
|
if (rejectUnauthenticated(ctx, req, res)) return;
|
|
975
1058
|
if (req.method === 'OPTIONS') {
|
|
976
1059
|
res.statusCode = 204;
|
|
@@ -1001,6 +1084,16 @@ async function handleApi(ctx, req, res, service) {
|
|
|
1001
1084
|
sendJson(res, 200, await service.compare(body.a, body.b));
|
|
1002
1085
|
return;
|
|
1003
1086
|
}
|
|
1087
|
+
if (req.method === 'GET' && pathname === '/settings') {
|
|
1088
|
+
sendJson(res, 200, { tools: agentTools.status() });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (req.method === 'POST' && pathname === '/settings/tools') {
|
|
1092
|
+
const body = await readRequestBody(req);
|
|
1093
|
+
if (typeof body.enabled !== 'boolean') throw new Error('enabled must be a boolean');
|
|
1094
|
+
sendJson(res, 200, { tools: await agentTools.setEnabled(body.enabled) });
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1004
1097
|
const match = pathname.match(/^\/checkpoints\/([^/]+)$/);
|
|
1005
1098
|
if (req.method === 'GET' && match) {
|
|
1006
1099
|
sendJson(res, 200, await service.getCheckpoint(decodeURIComponent(match[1])));
|
|
@@ -1025,14 +1118,12 @@ export function apply(ctx, config = {}) {
|
|
|
1025
1118
|
const lifetime = new AbortController();
|
|
1026
1119
|
ctx.effect(() => () => { lifetime.abort(); }, 'llm-fingerprint: abort running captures');
|
|
1027
1120
|
const service = createService(ctx, root, lifetime.signal);
|
|
1028
|
-
|
|
1029
|
-
ctx.effect(() => ctx.tools.register(definition), `llm-fingerprint: ${definition.name}`);
|
|
1030
|
-
}
|
|
1121
|
+
const agentTools = createAgentTools(ctx, service, root);
|
|
1031
1122
|
ctx.effect(
|
|
1032
1123
|
() => ctx.webServer.register({
|
|
1033
1124
|
kind: 'prefix',
|
|
1034
1125
|
path: API_PREFIX,
|
|
1035
|
-
handler: (req, res) => handleApi(ctx, req, res, service),
|
|
1126
|
+
handler: (req, res) => handleApi(ctx, req, res, service, agentTools),
|
|
1036
1127
|
}),
|
|
1037
1128
|
'llm-fingerprint: http api',
|
|
1038
1129
|
);
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fingerprint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Jerry2262/dsh-fingerprint.git"
|
|
9
|
+
"url": "git+https://github.com/Jerry2262/dsh-fingerprint.git"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/Jerry2262/dsh-fingerprint",
|
|
12
12
|
"bugs": {
|
|
@@ -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": {
|