dsh-llm-verifier 0.1.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/LICENSE +22 -0
- package/README.md +218 -0
- package/cordis.patch.yml +4 -0
- package/lib/caller-BgqCctCh.js +324 -0
- package/lib/caller-CGlgZ-Su.js +324 -0
- package/lib/caller.js +2 -0
- package/lib/client.js +385 -0
- package/lib/core.js +163 -0
- package/lib/index.js +1031 -0
- package/lib/types/cache.d.ts +27 -0
- package/lib/types/cache.js +82 -0
- package/lib/types/caller.d.ts +48 -0
- package/lib/types/caller.js +125 -0
- package/lib/types/client.d.ts +4 -0
- package/lib/types/client.js +66 -0
- package/lib/types/config.d.ts +35 -0
- package/lib/types/config.js +57 -0
- package/lib/types/core.d.ts +38 -0
- package/lib/types/core.js +177 -0
- package/lib/types/engine.d.ts +79 -0
- package/lib/types/engine.js +146 -0
- package/lib/types/images.d.ts +3 -0
- package/lib/types/images.js +45 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/index.js +42 -0
- package/lib/types/session.d.ts +23 -0
- package/lib/types/session.js +67 -0
- package/lib/types/top-logprobs.d.ts +24 -0
- package/lib/types/top-logprobs.js +97 -0
- package/package.json +110 -0
- package/src/cache.ts +88 -0
- package/src/caller.test.ts +34 -0
- package/src/caller.ts +138 -0
- package/src/client.tsx +48 -0
- package/src/config.ts +84 -0
- package/src/core.test.ts +67 -0
- package/src/core.ts +204 -0
- package/src/engine.ts +109 -0
- package/src/images.ts +33 -0
- package/src/index.ts +48 -0
- package/src/parity.test.ts +71 -0
- package/src/session.test.ts +22 -0
- package/src/session.ts +75 -0
- package/src/top-logprobs.ts +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 llm-as-a-verifier (Original Implementation)
|
|
4
|
+
Copyright (c) 2026 DSH LLM Verifier Contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# DSH LLM Verifier
|
|
2
|
+
|
|
3
|
+
> 基于 [llm-as-a-verifier/llm-as-a-verifier](https://github.com/llm-as-a-verifier/llm-as-a-verifier) 迁移构建的 DSH 原生模型可配置复核插件(Verifier)。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 它是什么?
|
|
8
|
+
|
|
9
|
+
`dsh-llm-verifier` 为 DeepSeek Harness(DSH)引入了一套**独立的裁判复核机制**。在主 Agent 负责生成代码、执行命令与工具交互的同时,Verifier 会收集当前任务目标、各候选方案过程以及真实的终端执行结果,交由你在设置中指定的独立 DSH 模型进行仲裁:评估哪个方案更可靠、当前任务的实际完成进度、以及是否存在未发现的潜在错误。
|
|
10
|
+
|
|
11
|
+
只有在主动调用以下工具时,插件才会向裁判模型发起请求:
|
|
12
|
+
|
|
13
|
+
- `verifier_compare`:对两个候选执行过程进行成对比较(Pairwise Comparison);
|
|
14
|
+
- `verifier_select`:在多个候选方案中通过锦标赛机制选出最优解;
|
|
15
|
+
- `verifier_track`:评估任务在不同检查点(Checkpoint)的完成度与进展;
|
|
16
|
+
- `verifier_current_session`:显式提取当前 DSH 会话记录,进行脱敏并执行复核。
|
|
17
|
+
|
|
18
|
+
## 安装与启用 (Installation & Usage)
|
|
19
|
+
|
|
20
|
+
### 1. 使用 DSH 插件命令安装
|
|
21
|
+
|
|
22
|
+
通过 DSH 提供的 `dsh plugin` 命令将本插件安装至对应的 Profile(如 `web` 或 `tui`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 为 Web Profile 安装插件
|
|
26
|
+
dsh plugin --profile web add dsh-llm-verifier
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. 载入与可视化配置
|
|
30
|
+
|
|
31
|
+
- **常规启动**:安装完成后直接启动对应 Profile(如 `dsh web`),插件将自动载入。
|
|
32
|
+
- **自定义 Patch Overlay(可选)**:若在自定义 Profile 清单中手动声明,可在 `cordis.patch.yml` 中配置:
|
|
33
|
+
```yaml
|
|
34
|
+
- insert:
|
|
35
|
+
- id: llm-verifier
|
|
36
|
+
name: 'dsh-llm-verifier'
|
|
37
|
+
```
|
|
38
|
+
- **配置面板**:启动 DSH 后打开 **`设置 → LLM Verifier`** 即可可视化配置裁判所使用的 Provider、Model、推理强度、并发限制与缓存策略。
|
|
39
|
+
|
|
40
|
+
### 3. 作为独立库引用(可选)
|
|
41
|
+
|
|
42
|
+
你也可以在 TypeScript / JavaScript 项目中直接引用核心算法与评分函数:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import {
|
|
46
|
+
extractScore,
|
|
47
|
+
extractProgressScore,
|
|
48
|
+
bradleyTerry,
|
|
49
|
+
pivotRoundPairs,
|
|
50
|
+
} from 'dsh-llm-verifier/core'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 迁移来源
|
|
54
|
+
|
|
55
|
+
本插件的核心评估理论、A–T 评分标尺、进度判定算法以及概率基准锦标赛(Probabilistic Pivot Tournament)均源自开源项目:
|
|
56
|
+
|
|
57
|
+
- **上游项目**:[llm-as-a-verifier/llm-as-a-verifier](https://github.com/llm-as-a-verifier/llm-as-a-verifier)
|
|
58
|
+
|
|
59
|
+
本插件将上游算法完整移植为 TypeScript 实现,并深度集成了 DSH 的模型路由、设置面板、附件管理、会话日志与工具生态。上游实现作为本插件的算法基准;本仓库内置了 Python / TypeScript 一致性测试(Parity Tests),确保核心评分与锦标赛赛制(Tournament Fixture)的跨语言逻辑完全一致。
|
|
60
|
+
|
|
61
|
+
## 核心原理
|
|
62
|
+
|
|
63
|
+
可以将整体协作模式理解为 **“选手 + 裁判”**:
|
|
64
|
+
|
|
65
|
+
1. **主 Agent(选手)**:专注于具体任务的执行(编写代码、运行命令、执行单元测试);
|
|
66
|
+
2. **Verifier(现场记录)**:收集题目要求、候选代码/解答及真实的终端输出证据;
|
|
67
|
+
3. **独立模型(裁判)**:在 DSH 设置中自由选择可用模型担任裁判;
|
|
68
|
+
4. **输出判定**:裁判必须在 **A–T** 细粒度标尺中给出明确的判定(Verdict);
|
|
69
|
+
5. **分数换算与聚合**:插件将字母判定换算为 0–1 区间的分数,并在多个评判维度、重复轮次以及交换位置后取加权平均;
|
|
70
|
+
6. **多候选优化**:在多候选方案场景下启用 Pivot Tournament 算法,避免高成本的全量两两比对。
|
|
71
|
+
|
|
72
|
+
### 一次成对比较的完整流程
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart TD
|
|
76
|
+
U[任务目标 Problem] --> P[构造裁判提示词]
|
|
77
|
+
A[候选 A 与真实工具输出] --> P
|
|
78
|
+
B[候选 B 与真实工具输出] --> P
|
|
79
|
+
P --> C{持久缓存是否命中?}
|
|
80
|
+
C -- 是 --> R[直接复用历史评分]
|
|
81
|
+
C -- 否 --> D[通过 DSH LLM Runtime 调用指定裁判模型]
|
|
82
|
+
D --> V[模型输出 A–T Verdict]
|
|
83
|
+
V --> S[换算为 0–1 分数]
|
|
84
|
+
S --> W[写入持久化成功缓存]
|
|
85
|
+
W --> R
|
|
86
|
+
R --> O[按评估标准与重复轮次聚合]
|
|
87
|
+
O --> X[返回胜出者、得分、Token 消耗及缓存统计]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 如何消除位置偏见(Position Bias)?
|
|
91
|
+
|
|
92
|
+
大语言模型通常存在“首位偏好(First-token Bias)”或对特定选项标签的偏置。为此,插件默认在每个评判标准下进行两轮对决:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
第 1 轮:候选 A 放置于左侧,候选 B 放置于右侧
|
|
96
|
+
第 2 轮:候选 B 放置于左侧,候选 A 放置于右侧
|
|
97
|
+
最 终:将第 2 轮结果还原至原始候选身份后,与第 1 轮计算平均得分
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
通过位置对称交换与结果求平均,最大程度抵消模型的位置偏置干扰。
|
|
101
|
+
|
|
102
|
+
### A–T 细粒度标尺说明
|
|
103
|
+
|
|
104
|
+
在成对比较(Pairwise)中,标尺定义如下:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
A = 明确、完整、有确凿证据证明成功
|
|
108
|
+
B ... J = 置信度依次递减,但整体偏向成功
|
|
109
|
+
K ... S = 偏向失败的程度依次递增
|
|
110
|
+
T = 明确且无可争议的失败
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
字母按等距标尺线性映射到 `[0, 1]` 区间。在打分计算上,插件采用**自适应评分机制**:
|
|
114
|
+
|
|
115
|
+
- **优先概率期望评分**:若当前模型路由为官方 `deepseek-official` 或显式配置了兼容 OpenAI Chat Completions 且支持 `top_logprobs` 的端点,插件将基于 A–T 候选 Token 的完整概率分布计算加权期望值;
|
|
116
|
+
- **自适应降级显式标签**:若当前路由不支持、供应商拒绝返回 `logprobs`,或响应中缺失 Token 概率,则平滑回退至模型最终输出的 A–T 显式标签。
|
|
117
|
+
- *原则:插件不会仅凭供应商或模型名称预设其能力,始终以运行时实际响应结果为准。*
|
|
118
|
+
|
|
119
|
+
> [!NOTE]
|
|
120
|
+
> 在**进度追踪(`verifier_track`)**中,字母方向相反:`A` 代表“确定尚未完成”,`T` 代表“几乎确定已完成”,再按规则映射为 `[0, 1]` 的完成度概率。
|
|
121
|
+
|
|
122
|
+
## 多候选方案:为什么不采用全量两两比对?
|
|
123
|
+
|
|
124
|
+
对 $N$ 个候选方案进行全量比对(All-Pairs Tournament)需要进行约 $\frac{N(N - 1)}{2}$ 场对决,API 调用量呈二次方($O(N^2)$)爆炸式增长。
|
|
125
|
+
|
|
126
|
+
插件引入了上游的 **Probabilistic Pivot Tournament(概率基准锦标赛)**:
|
|
127
|
+
|
|
128
|
+
```mermaid
|
|
129
|
+
flowchart LR
|
|
130
|
+
C[所有候选方案] --> H[构建随机 Hamiltonian Ring]
|
|
131
|
+
H --> N[仅比对环上相邻候选]
|
|
132
|
+
N --> P[筛选出 top-k 基准候选 Pivots]
|
|
133
|
+
P --> Q[其余候选仅与 Pivots 进行对决]
|
|
134
|
+
Q --> BT[Bradley–Terry 软胜率建模]
|
|
135
|
+
BT --> Rank[生成最终全局排名]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
比对复杂度显著降低至接近 $O(Nk)$(其中 $k \ll N$)。候选方案越多,节省的 API 请求与 Token 成本越明显。
|
|
139
|
+
|
|
140
|
+
## 配置项说明
|
|
141
|
+
|
|
142
|
+
在 DSH 中打开:`设置 → LLM Verifier`。
|
|
143
|
+
|
|
144
|
+
| 配置项 | 说明 |
|
|
145
|
+
|---|---|
|
|
146
|
+
| **供应商 (Provider)** | 从 DSH 当前已配置且可路由的 Provider 列表中选择 |
|
|
147
|
+
| **模型 (Model)** | 从所选 Provider 的模型目录中指定具体裁判模型 |
|
|
148
|
+
| **推理强度 (Reasoning Effort)** | 使用 Adapter 为该模型声明的思考强度,或保留模型默认值 |
|
|
149
|
+
| **最大输出 Token** | 单次裁判请求的输出 Token 上限 |
|
|
150
|
+
| **最大并发数** | 所有 Verifier 调用共享的全局并发请求限制 |
|
|
151
|
+
| **最大重试次数** | 遇到偶发网络或服务短暂故障时的额外重试次数 |
|
|
152
|
+
| **请求超时 (ms)** | 单次模型调用的毫秒级超时时间 |
|
|
153
|
+
| **缓存条目上限** | 成功评分持久化缓存的最大容量条目数 |
|
|
154
|
+
| **输入/输出价格** | 仅用于本地估算单次裁判产生的费用(`estimatedCostUsd`) |
|
|
155
|
+
|
|
156
|
+
> [!NOTE]
|
|
157
|
+
> **多模态与图片支持**:若选定的裁判模型不支持图像输入,传入图片时将由对应 DSH Adapter 明确报错拦截,插件绝不会静默丢弃图片证据。
|
|
158
|
+
|
|
159
|
+
## 缓存、重试与遥测统计
|
|
160
|
+
|
|
161
|
+
- **持久化缓存**:成功的评分结果将基于 SHA-256 哈希值进行持久化缓存。Key 的计算维度包含:Provider、Model、任务描述、候选内容、评审标准、轮次编号、参数配置及图片摘要。失败或被取消的请求不会写入缓存。
|
|
162
|
+
- **并发请求合并**:相同的并发比对请求会自动共用在途 Promise(In-flight Deduplication),避免重复调用。
|
|
163
|
+
- **运行统计与监控**:每次调用返回完整的统计信息,涵盖实际请求次数、重试次数(Retries)、各类 Token 消耗(输入/缓存命中/输出/推理 Token)、缓存命中与未命中次数(Cache Hits/Misses)以及费用估算;其中:
|
|
164
|
+
- `topLogprobScores`:采用概率分布期望计算的评分次数;
|
|
165
|
+
- `explicitTagScores`:降级采用模型显式输出标签的评分次数。
|
|
166
|
+
- **能力记忆**:当某个 Provider/Model 明确拒绝 `logprobs` 参数后,插件进程会记住该能力探测结果,避免后续每次调用重复报错。
|
|
167
|
+
|
|
168
|
+
## 隐私与数据安全边界
|
|
169
|
+
|
|
170
|
+
`verifier_current_session` 工具**仅在被显式调用时**才会读取当前 DSH 会话内容:
|
|
171
|
+
|
|
172
|
+
- **提取范围**:仅提取直接的用户消息、Assistant 回复、工具调用(Tool Call)及实际工具执行结果(Tool Result);自动排除插件和系统内置指令(Plugin/System Instructions);
|
|
173
|
+
- **敏感信息脱敏**:默认对常见 Bearer Token、API Key、通用 Token、Password 及 Secret 进行脱敏替换;
|
|
174
|
+
- **灵活控制**:支持指定提取的消息序号范围、字符长度截断上限以及自定义正则表达式脱敏规则;
|
|
175
|
+
- **数据流向**:提取并脱敏后的上下文仅会发送给你在设置面板中指定的裁判模型。
|
|
176
|
+
|
|
177
|
+
> [!IMPORTANT]
|
|
178
|
+
> 默认脱敏规则不能替代全面的数据防泄漏(DLP)策略。在处理高度敏感的任务时,建议主动限制提取范围并补充自定义脱敏正则表达式。
|
|
179
|
+
|
|
180
|
+
## 自动概率评分机制 (Automatic Probability Scoring)
|
|
181
|
+
|
|
182
|
+
插件默认优先请求 `top_logprobs: 20`。它通过读取判定标签后第一个 Token 的 A–T 候选分布,经重新归一化后计算得分期望值。仅在当前路由通道无法提供概率分布时,才降级使用模型生成的最终文本标签。
|
|
183
|
+
|
|
184
|
+
- **支持直接透传 logprobs 的通道**:官方 `deepseek-official` 路由,以及在 DSH `llm-pi-ai` 中明确声明 `api: openai-completions` 且使用 HTTPS `baseURL` 的路由;
|
|
185
|
+
- **回退通道**:其他私有 Adapter 则通过标准 DSH Stream 稳定回退到显式标签模式。
|
|
186
|
+
|
|
187
|
+
结果中的模式计数使插件行为完全透明可观测,而非静默假设所有模型都具备概率输出能力。
|
|
188
|
+
|
|
189
|
+
### 验证评分模式
|
|
190
|
+
|
|
191
|
+
使用一个不会命中历史缓存的新问题并设置 `repeats: 1`。运行 `verifier_compare` 后观察返回的 `stats`:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"stats": {
|
|
196
|
+
"topLogprobScores": 3,
|
|
197
|
+
"explicitTagScores": 0
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
默认包含 3 个评判标准(Criteria),因此在完全支持概率期望的模型上通常为 `3 / 0`;在不支持 logprobs 的模型上通常为 `0 / 3`。当不同标准或并发探测遇到服务商策略差异时,也可能呈现混合数值。缓存键已经过版本升级,旧的纯标签缓存不会被误用为新的自动策略结果。
|
|
203
|
+
|
|
204
|
+
## 上游授权与致谢
|
|
205
|
+
|
|
206
|
+
本插件的核心思想与算法实现移植自开源项目 [llm-as-a-verifier/llm-as-a-verifier](https://github.com/llm-as-a-verifier/llm-as-a-verifier)。上游项目在其 `LICENSE` 与 `pyproject.toml` 中明确采用 **MIT License**,原始版权声明如下:
|
|
207
|
+
|
|
208
|
+
> Copyright (c) 2026 llm-as-a-verifier
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
The MIT License permits use, copying, modification, merging, publication,
|
|
212
|
+
distribution, sublicensing, and sale, provided that the copyright and
|
|
213
|
+
permission notices are preserved in copies or substantial portions. The
|
|
214
|
+
software is provided “AS IS”, without warranty. See the upstream LICENSE
|
|
215
|
+
for the complete legal text.
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
衷心感谢上游项目的作者及贡献者开源了细粒度 A–T 奖励机制、Token 对数概率期望打分、任务进度验证、Bradley–Terry 软胜率模型以及 Probabilistic Pivot Tournament 锦标赛算法。本 DSH 插件将这一系列优秀成果移植并深度整合至 TypeScript 与 DeepSeek Harness 生态中;本插件不对上述理论算法声明原创所有权。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
|
+
import { BlockAssembler, ReasoningEffortId, createUserMessage, deepFreeze } from "@deepseek-ai/dsh-llm";
|
|
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 (!settings) return void 0;
|
|
30
|
+
if (provider === "deepseek-official") {
|
|
31
|
+
const value = object(settings.get(settingsNamespace("llm-deepseek"))) ?? {};
|
|
32
|
+
const apiKey = await credential(ctx, text(value.apiKeyEnv) ?? "DEEPSEEK_API_KEY");
|
|
33
|
+
if (!apiKey) return void 0;
|
|
34
|
+
return {
|
|
35
|
+
baseURL: text(value.baseURL) ?? "https://api.deepseek.com",
|
|
36
|
+
apiKey,
|
|
37
|
+
deepSeekThinking: true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
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|unsupported|unknown|invalid|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-BgqCctCh.js.map
|