dsh-llm-verifier 0.1.2 → 0.1.4
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 +13 -6
- package/lib/caller-BXrUTAoL.js +324 -0
- package/lib/caller.js +1 -1
- package/lib/client.js +275 -114
- package/lib/index.js +301 -53
- package/lib/types/auto.d.ts +26 -0
- package/lib/types/auto.js +86 -0
- package/lib/types/client.js +16 -11
- package/lib/types/config.d.ts +15 -0
- package/lib/types/config.js +21 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +42 -10
- package/package.json +1 -1
- package/src/auto.test.ts +69 -0
- package/src/auto.ts +113 -0
- package/src/client.tsx +57 -27
- package/src/config.ts +34 -1
- package/src/index.ts +45 -12
package/README.md
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
`dsh-llm-verifier` 为 DeepSeek Harness(DSH)引入了一套**独立的裁判复核机制**。在主 Agent 负责生成代码、执行命令与工具交互的同时,Verifier 会收集当前任务目标、各候选方案过程以及真实的终端执行结果,交由你在设置中指定的独立 DSH 模型进行仲裁:评估哪个方案更可靠、当前任务的实际完成进度、以及是否存在未发现的潜在错误。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
插件提供四个显式工具,并支持可配置的宿主级自动会话验收:
|
|
12
12
|
|
|
13
13
|
- `verifier_compare`:对两个候选执行过程进行成对比较(Pairwise Comparison);
|
|
14
|
-
- `verifier_select
|
|
15
|
-
- `verifier_track
|
|
16
|
-
- `verifier_current_session`:显式提取当前 DSH
|
|
14
|
+
- `verifier_select`:在多个候选方案中通过锦标赛机制选出最优解,供 Best-of-N / 多候选编排器直接调用;
|
|
15
|
+
- `verifier_track`:评估任务在已有检查点(Checkpoint)的完成度与进展,供 Goal / Workflow 等长任务编排器直接调用;
|
|
16
|
+
- `verifier_current_session`:显式提取当前 DSH 会话记录,进行脱敏并执行复核;
|
|
17
|
+
- **自动验收门控**:智能或严格策略在 `agent/turn-stopping` 生命周期边界检测真实工具证据,宿主直接运行同一会话验收逻辑;未通过时以插件 steering 反馈要求 Agent 修复并重新验证,而不是依赖模型是否主动想起工具。
|
|
17
18
|
|
|
18
19
|
## 安装与启用 (Installation & Usage)
|
|
19
20
|
|
|
@@ -164,7 +165,13 @@ flowchart LR
|
|
|
164
165
|
|
|
165
166
|
| 配置项 | 说明 |
|
|
166
167
|
|---|---|
|
|
167
|
-
| **启用工具 (Enabled)** |
|
|
168
|
+
| **启用工具 (Enabled)** | 是否允许显式工具与自动验收向裁判模型发起请求;关闭后显式调用立即报错且自动门控不运行 |
|
|
169
|
+
| **调用策略** | `manual` 仅显式调用;`smart` 仅在达到工具证据数量且含写入/执行类操作时自动验收;`strict` 对任一已完成的关键操作自动验收 |
|
|
170
|
+
| **通过阈值** | 自动会话验收要求证据分数达到阈值且胜过“未执行有效工作”基线 |
|
|
171
|
+
| **自动评估轮次** | 自动验收每个标准的重复轮次;默认 1,作为低成本初筛 |
|
|
172
|
+
| **智能模式最少工具调用** | `smart` 策略需要的最少非 Verifier 工具调用数 |
|
|
173
|
+
| **最大证据字符** | 自动发送给裁判的最近会话轨迹字符上限 |
|
|
174
|
+
| **每任务/每会话最多验收** | 防止低分反馈形成无限修复循环并限制成本 |
|
|
168
175
|
| **供应商 (Provider)** | 从 DSH 当前已配置且可路由的 Provider 列表中选择 |
|
|
169
176
|
| **模型 (Model)** | 从所选 Provider 的模型目录中指定具体裁判模型 |
|
|
170
177
|
| **推理强度 (Reasoning Effort)** | 使用 Adapter 为该模型声明的思考强度,或保留模型默认值 |
|
|
@@ -189,7 +196,7 @@ flowchart LR
|
|
|
189
196
|
|
|
190
197
|
## 隐私与数据安全边界
|
|
191
198
|
|
|
192
|
-
`verifier_current_session`
|
|
199
|
+
`verifier_current_session` 会在显式调用时读取当前 DSH 会话;启用 `smart` / `strict` 策略后,宿主也会在满足证据门槛的任务结束边界自动调用同一提取与复核路径:
|
|
193
200
|
|
|
194
201
|
- **提取范围**:仅提取直接的用户消息、Assistant 回复、工具调用(Tool Call)及实际工具执行结果(Tool Result);自动排除插件和系统内置指令(Plugin/System Instructions);
|
|
195
202
|
- **敏感信息脱敏**:默认对常见 Bearer Token、API Key、通用 Token、Password 及 Secret 进行脱敏替换;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { BlockAssembler, ReasoningEffortId, createUserMessage, deepFreeze } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
|
+
//#region src/top-logprobs.ts
|
|
5
|
+
var TopLogprobsUnsupportedError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "TopLogprobsUnsupportedError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function object(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
13
|
+
}
|
|
14
|
+
function text(value) {
|
|
15
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
16
|
+
}
|
|
17
|
+
function endpoint(baseURL) {
|
|
18
|
+
return baseURL.replace(/\/+$/, "") + "/chat/completions";
|
|
19
|
+
}
|
|
20
|
+
function dataUrl(image) {
|
|
21
|
+
return "data:" + image.mediaType + ";base64," + Buffer.from(image.data.buffer, image.data.byteOffset, image.data.byteLength).toString("base64");
|
|
22
|
+
}
|
|
23
|
+
async function credential(ctx, name) {
|
|
24
|
+
if (!name) return void 0;
|
|
25
|
+
return (await ctx.get("credentials")?.resolve(credentialRef(name)))?.value;
|
|
26
|
+
}
|
|
27
|
+
async function resolveTopLogprobRoute(ctx, provider) {
|
|
28
|
+
const settings = ctx.get("settings");
|
|
29
|
+
if (provider === "deepseek-official") {
|
|
30
|
+
const value = settings ? object(settings.get(settingsNamespace("llm-deepseek"))) ?? {} : {};
|
|
31
|
+
const apiKey = await credential(ctx, text(value.apiKeyEnv) ?? "DEEPSEEK_API_KEY");
|
|
32
|
+
if (!apiKey) return void 0;
|
|
33
|
+
return {
|
|
34
|
+
baseURL: text(value.baseURL) ?? "https://api.deepseek.com",
|
|
35
|
+
apiKey,
|
|
36
|
+
deepSeekThinking: true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (!settings) return void 0;
|
|
40
|
+
const profile = object(object(object(settings.get(settingsNamespace("llm-pi-ai")))?.providers)?.[provider]);
|
|
41
|
+
if (!profile || profile.api !== "openai-completions") return void 0;
|
|
42
|
+
const baseURL = text(profile.baseURL);
|
|
43
|
+
if (!baseURL || !/^https:\/\//i.test(baseURL)) return void 0;
|
|
44
|
+
const apiKey = await credential(ctx, text(profile.apiKeyEnv));
|
|
45
|
+
const rawHeaders = object(profile.headers);
|
|
46
|
+
const headers = rawHeaders === void 0 ? void 0 : Object.fromEntries(Object.entries(rawHeaders).filter((entry) => typeof entry[1] === "string"));
|
|
47
|
+
return {
|
|
48
|
+
baseURL,
|
|
49
|
+
...apiKey ? { apiKey } : {},
|
|
50
|
+
...headers ? { headers } : {},
|
|
51
|
+
deepSeekThinking: false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function callTopLogprobs(route, model, prompt, maxTokens, reasoningEffort, signal, images) {
|
|
55
|
+
const content = images?.length ? [{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: prompt
|
|
58
|
+
}, ...images.map((image) => ({
|
|
59
|
+
type: "image_url",
|
|
60
|
+
image_url: { url: dataUrl(image) }
|
|
61
|
+
}))] : prompt;
|
|
62
|
+
const thinking = route.deepSeekThinking && reasoningEffort ? reasoningEffort === "off" ? { thinking: { type: "disabled" } } : {
|
|
63
|
+
thinking: { type: "enabled" },
|
|
64
|
+
reasoning_effort: reasoningEffort
|
|
65
|
+
} : {};
|
|
66
|
+
const response = await fetch(endpoint(route.baseURL), {
|
|
67
|
+
method: "POST",
|
|
68
|
+
redirect: "error",
|
|
69
|
+
signal,
|
|
70
|
+
headers: {
|
|
71
|
+
"content-type": "application/json",
|
|
72
|
+
...route.apiKey ? { authorization: "Bearer " + route.apiKey } : {},
|
|
73
|
+
...route.headers
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
model,
|
|
77
|
+
messages: [{
|
|
78
|
+
role: "user",
|
|
79
|
+
content
|
|
80
|
+
}],
|
|
81
|
+
max_tokens: maxTokens,
|
|
82
|
+
temperature: 1,
|
|
83
|
+
logprobs: true,
|
|
84
|
+
top_logprobs: 20,
|
|
85
|
+
...thinking
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
const raw = await response.text();
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const excerpt = raw.slice(0, 1e3);
|
|
91
|
+
if ([
|
|
92
|
+
400,
|
|
93
|
+
404,
|
|
94
|
+
405,
|
|
95
|
+
415,
|
|
96
|
+
422
|
|
97
|
+
].includes(response.status) && /logprob|top_logprobs|unsupported|unknown (?:field|parameter)|unrecognized (?:field|parameter)|not support/i.test(excerpt)) throw new TopLogprobsUnsupportedError("provider rejected top_logprobs: HTTP " + response.status + " " + excerpt);
|
|
98
|
+
throw new Error("llm-verifier: top_logprobs request failed with HTTP " + response.status + ": " + excerpt);
|
|
99
|
+
}
|
|
100
|
+
let body;
|
|
101
|
+
try {
|
|
102
|
+
body = object(JSON.parse(raw)) ?? {};
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error("llm-verifier: top_logprobs endpoint returned invalid JSON");
|
|
105
|
+
}
|
|
106
|
+
const choice = object((Array.isArray(body.choices) ? body.choices : [])[0]);
|
|
107
|
+
const message = object(choice?.message);
|
|
108
|
+
const answer = typeof message?.content === "string" ? message.content : "";
|
|
109
|
+
const logprobs = object(choice?.logprobs);
|
|
110
|
+
const rows = Array.isArray(logprobs?.content) ? logprobs.content : [];
|
|
111
|
+
if (!rows.length) throw new TopLogprobsUnsupportedError("provider returned no token logprobs");
|
|
112
|
+
const tokens = [];
|
|
113
|
+
const positions = [];
|
|
114
|
+
for (const rawRow of rows) {
|
|
115
|
+
const row = object(rawRow) ?? {};
|
|
116
|
+
const token = typeof row.token === "string" ? row.token : "";
|
|
117
|
+
tokens.push(token);
|
|
118
|
+
const alternatives = (Array.isArray(row.top_logprobs) ? row.top_logprobs : []).flatMap((value) => {
|
|
119
|
+
const item = object(value);
|
|
120
|
+
return item && typeof item.token === "string" && typeof item.logprob === "number" ? [{
|
|
121
|
+
token: item.token,
|
|
122
|
+
logprob: item.logprob
|
|
123
|
+
}] : [];
|
|
124
|
+
});
|
|
125
|
+
if (!alternatives.length && typeof row.logprob === "number") alternatives.push({
|
|
126
|
+
token,
|
|
127
|
+
logprob: row.logprob
|
|
128
|
+
});
|
|
129
|
+
positions.push(alternatives);
|
|
130
|
+
}
|
|
131
|
+
const rawUsage = object(body.usage) ?? {};
|
|
132
|
+
const promptDetails = object(rawUsage.prompt_tokens_details) ?? {};
|
|
133
|
+
const completionDetails = object(rawUsage.completion_tokens_details) ?? {};
|
|
134
|
+
const cached = Number(rawUsage.prompt_cache_hit_tokens ?? promptDetails.cached_tokens ?? 0) || 0;
|
|
135
|
+
const input = Number(rawUsage.prompt_tokens ?? 0) || 0;
|
|
136
|
+
return {
|
|
137
|
+
text: answer,
|
|
138
|
+
tokens,
|
|
139
|
+
positions,
|
|
140
|
+
scoringMode: "top-logprobs",
|
|
141
|
+
usage: {
|
|
142
|
+
calls: 1,
|
|
143
|
+
attempts: 1,
|
|
144
|
+
retries: 0,
|
|
145
|
+
inputTokens: Math.max(0, input - cached),
|
|
146
|
+
cachedInputTokens: cached,
|
|
147
|
+
outputTokens: Number(rawUsage.completion_tokens ?? 0) || 0,
|
|
148
|
+
reasoningTokens: Number(completionDetails.reasoning_tokens ?? 0) || 0
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
var TopLogprobCapabilityCache = class {
|
|
153
|
+
unsupported = /* @__PURE__ */ new Set();
|
|
154
|
+
isUnsupported(provider, model) {
|
|
155
|
+
return this.unsupported.has(provider + "\0" + model);
|
|
156
|
+
}
|
|
157
|
+
markUnsupported(provider, model) {
|
|
158
|
+
this.unsupported.add(provider + "\0" + model);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/caller.ts
|
|
163
|
+
function failureMessage(finish) {
|
|
164
|
+
if (finish.kind === "error" || finish.kind === "aborted") return finish.failure.message;
|
|
165
|
+
if (finish.kind === "max-tokens") return "verifier response reached max tokens before completing its answer";
|
|
166
|
+
}
|
|
167
|
+
async function delay(ms, signal) {
|
|
168
|
+
if (signal?.aborted) throw signal.reason;
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
const timer = setTimeout(resolve, ms);
|
|
171
|
+
const abort = () => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
reject(signal?.reason);
|
|
174
|
+
};
|
|
175
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function usage(attempts, value = {}) {
|
|
179
|
+
return {
|
|
180
|
+
calls: 1,
|
|
181
|
+
attempts,
|
|
182
|
+
retries: attempts - 1,
|
|
183
|
+
inputTokens: value.inputTokens ?? 0,
|
|
184
|
+
cachedInputTokens: (value.cacheReadTokens ?? 0) + (value.cacheWriteTokens ?? 0),
|
|
185
|
+
outputTokens: value.outputTokens ?? 0,
|
|
186
|
+
reasoningTokens: value.reasoningTokens ?? 0
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async function callExplicitTag(config, prompt, signal, images) {
|
|
190
|
+
let attempt = 0;
|
|
191
|
+
while (true) {
|
|
192
|
+
attempt += 1;
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("llm-verifier: request timed out")), config.timeoutMs);
|
|
195
|
+
const abort = () => controller.abort(signal?.reason);
|
|
196
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
197
|
+
try {
|
|
198
|
+
const content = [{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: prompt
|
|
201
|
+
}];
|
|
202
|
+
for (const image of images ?? []) {
|
|
203
|
+
const ref = await config.attachments.saveImage({
|
|
204
|
+
data: image.data,
|
|
205
|
+
mediaType: image.mediaType
|
|
206
|
+
});
|
|
207
|
+
content.push({
|
|
208
|
+
type: "image",
|
|
209
|
+
attachment: ref
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const messages = [createUserMessage({
|
|
213
|
+
content,
|
|
214
|
+
source: {
|
|
215
|
+
kind: "plugin",
|
|
216
|
+
plugin: "dsh-llm-verifier"
|
|
217
|
+
}
|
|
218
|
+
})];
|
|
219
|
+
const assembler = new BlockAssembler();
|
|
220
|
+
const options = deepFreeze({
|
|
221
|
+
provider: config.provider,
|
|
222
|
+
model: config.model,
|
|
223
|
+
...config.reasoningEffort ? { reasoningEffort: ReasoningEffortId(config.reasoningEffort) } : {},
|
|
224
|
+
messages,
|
|
225
|
+
maxTokens: config.maxTokens,
|
|
226
|
+
temperature: 1,
|
|
227
|
+
signal: controller.signal
|
|
228
|
+
});
|
|
229
|
+
for await (const chunk of config.llm.stream(options)) assembler.push(chunk);
|
|
230
|
+
const failed = failureMessage(assembler.finish);
|
|
231
|
+
if (failed !== void 0) throw new Error("llm-verifier: model call failed: " + failed);
|
|
232
|
+
const text = assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
233
|
+
if (!text.trim()) throw new Error("llm-verifier: selected DSH model produced no text");
|
|
234
|
+
return {
|
|
235
|
+
text,
|
|
236
|
+
tokens: [],
|
|
237
|
+
positions: [],
|
|
238
|
+
scoringMode: "explicit-tag",
|
|
239
|
+
usage: usage(attempt, assembler.usage)
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (signal?.aborted) throw signal.reason;
|
|
243
|
+
if (attempt > config.maxRetries || !(error instanceof Error) || !/rate|quota|timeout|timed out|temporar|network|fetch|socket|5dd/i.test(error.message)) throw error;
|
|
244
|
+
await delay(Math.min(3e4, config.retryBaseDelayMs * 2 ** (attempt - 1) * (.8 + Math.random() * .4)), signal);
|
|
245
|
+
} finally {
|
|
246
|
+
clearTimeout(timeout);
|
|
247
|
+
signal?.removeEventListener("abort", abort);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
var RequestLimiter = class {
|
|
252
|
+
limit;
|
|
253
|
+
active = 0;
|
|
254
|
+
queue = [];
|
|
255
|
+
constructor(limit) {
|
|
256
|
+
this.limit = limit;
|
|
257
|
+
}
|
|
258
|
+
async run(operation, signal) {
|
|
259
|
+
if (this.active >= this.limit) await new Promise((resolve, reject) => {
|
|
260
|
+
const enter = () => {
|
|
261
|
+
signal?.removeEventListener("abort", abort);
|
|
262
|
+
resolve();
|
|
263
|
+
};
|
|
264
|
+
const abort = () => {
|
|
265
|
+
const index = this.queue.indexOf(enter);
|
|
266
|
+
if (index >= 0) this.queue.splice(index, 1);
|
|
267
|
+
reject(signal?.reason);
|
|
268
|
+
};
|
|
269
|
+
this.queue.push(enter);
|
|
270
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
271
|
+
});
|
|
272
|
+
if (signal?.aborted) throw signal.reason;
|
|
273
|
+
this.active += 1;
|
|
274
|
+
try {
|
|
275
|
+
return await operation();
|
|
276
|
+
} finally {
|
|
277
|
+
this.active -= 1;
|
|
278
|
+
this.queue.shift()?.();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
async function callAutomatic(config, prompt, signal, images) {
|
|
283
|
+
if (!config.topLogprobCapabilities.isUnsupported(config.provider, config.model)) {
|
|
284
|
+
const route = await resolveTopLogprobRoute(config.ctx, config.provider);
|
|
285
|
+
if (route !== void 0) try {
|
|
286
|
+
return await callTopLogprobs(route, config.model, prompt, config.maxTokens, config.reasoningEffort, signal, images);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (!(error instanceof TopLogprobsUnsupportedError)) throw error;
|
|
289
|
+
config.topLogprobCapabilities.markUnsupported(config.provider, config.model);
|
|
290
|
+
}
|
|
291
|
+
else config.topLogprobCapabilities.markUnsupported(config.provider, config.model);
|
|
292
|
+
}
|
|
293
|
+
return callExplicitTag(config, prompt, signal, images);
|
|
294
|
+
}
|
|
295
|
+
async function callVerifier(config, prompt, signal, images) {
|
|
296
|
+
const invoke = () => callAutomatic(config, prompt, signal, images);
|
|
297
|
+
return config.limiter === void 0 ? invoke() : config.limiter.run(invoke, signal);
|
|
298
|
+
}
|
|
299
|
+
function addUsage(target, source) {
|
|
300
|
+
for (const key of [
|
|
301
|
+
"calls",
|
|
302
|
+
"attempts",
|
|
303
|
+
"retries",
|
|
304
|
+
"inputTokens",
|
|
305
|
+
"cachedInputTokens",
|
|
306
|
+
"outputTokens",
|
|
307
|
+
"reasoningTokens"
|
|
308
|
+
]) target[key] += source[key];
|
|
309
|
+
}
|
|
310
|
+
function emptyUsage() {
|
|
311
|
+
return {
|
|
312
|
+
calls: 0,
|
|
313
|
+
attempts: 0,
|
|
314
|
+
retries: 0,
|
|
315
|
+
inputTokens: 0,
|
|
316
|
+
cachedInputTokens: 0,
|
|
317
|
+
outputTokens: 0,
|
|
318
|
+
reasoningTokens: 0
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
export { TopLogprobCapabilityCache as a, emptyUsage as i, addUsage as n, callVerifier as r, RequestLimiter as t };
|
|
323
|
+
|
|
324
|
+
//# sourceMappingURL=caller-BXrUTAoL.js.map
|
package/lib/caller.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as emptyUsage, n as addUsage, r as callVerifier, t as RequestLimiter } from "./caller-
|
|
1
|
+
import { i as emptyUsage, n as addUsage, r as callVerifier, t as RequestLimiter } from "./caller-BXrUTAoL.js";
|
|
2
2
|
export { RequestLimiter, addUsage, callVerifier, emptyUsage };
|