dsh-plugin-model-tuning 2.0.1 → 2.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 +21 -21
- package/README.md +62 -1
- package/client.js +119 -11
- package/index.js +94 -9
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 dsh-plugin contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-plugin contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
- 系统提示角色兼容开关:`自动` / `system` / `developer`(仅 `llm-pi-ai`,
|
|
21
21
|
写入模型条目的 `compat.supportsDeveloperRole`;遇到 kimi/DeepSeek 等
|
|
22
22
|
报 `role 'developer' is not allowed` 时选 `system`);
|
|
23
|
+
- 思考格式兼容开关:`自动` / `自适应 thinking`(仅 `llm-pi-ai` 且路由 `api: anthropic-messages`,
|
|
24
|
+
写入模型条目的 `compat.forceAdaptiveThinking`;见下节「Anthropic 思考格式」。
|
|
25
|
+
该模型一个推理档位都没勾选时不显示——那时请求里根本没有 `thinking`);
|
|
23
26
|
- 生图模型勾选(把该模型写入「绘图」页签的模型列表,见「绘图页签」);
|
|
24
27
|
- 顶部搜索框按模型名 / ID / 描述 / provider 过滤;
|
|
25
28
|
- 修改即时保存(`remote.settings.mutate`;模型目录来自 `remote.session.modelCatalog()`)。
|
|
@@ -136,6 +139,59 @@ harness 的请求模态是**闭合集合 `text | image`**:`dsh-llm` 的 `Model
|
|
|
136
139
|
> 「系统提示角色」开关依赖 DSH 适配层 `dsh-llm-pi-ai` 透传 `compat.supportsDeveloperRole`
|
|
137
140
|
>(`0.1.0-rc.6` 默认不透传,需按本仓库 `deepseek-harness` 的 catalog.ts 补丁同步,或等上游合入)。
|
|
138
141
|
|
|
142
|
+
## Anthropic 思考格式(自适应 vs 预算式)
|
|
143
|
+
|
|
144
|
+
「模型参数」页的**思考格式**开关解决的是这一条报错:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
400 {"error":{"message":"claude-opus-5-5 requires adaptive thinking; omit thinking or use
|
|
148
|
+
thinking.type=adaptive and output_config.effort","type":"invalid_request_error"}}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **原因**:pi-ai 只有在模型的 `compat.forceAdaptiveThinking === true` 时才发
|
|
152
|
+
`thinking: {type:"adaptive"}` + `output_config: {effort:"…"}`;否则发旧的预算式
|
|
153
|
+
`thinking: {type:"enabled", budget_tokens:N}`。只接受自适应思考的模型(Claude Opus 4.6 及更新、
|
|
154
|
+
Sonnet 5、Fable 5、Kimi K3 等)会直接 400 拒掉预算式请求。
|
|
155
|
+
- **为什么手写路由会中招**:pi-ai 内置目录给它自己带的模型 ID 标了这个标记,
|
|
156
|
+
而 `dsh-llm-pi-ai` 只在「模型 ID 命中该 provider 自己的目录条目」时才继承目录 `compat`
|
|
157
|
+
(`resolveModelCompat` 要求 `base.api === api`)。手写路由(例如 `providers.ironman-claude`
|
|
158
|
+
这种目录里不存在的 key)没有任何目录条目可继承,`claude-opus-5-5` 这类目录里其实认识、
|
|
159
|
+
但挂在 `anthropic` 名下的 ID 也一样继承不到——于是请求退化成预算式。
|
|
160
|
+
- **怎么修**:在该模型的「思考格式」里选**自适应 thinking**,页面把
|
|
161
|
+
`compat.forceAdaptiveThinking: true` 写进模型条目(与 `系统提示角色` 同一套写法),
|
|
162
|
+
下一次请求即生效,无需重启。Claude 4.5 及更早只认预算式,保持「自动」。
|
|
163
|
+
- **另一条路(不动插件)**:把这条路由改成目录里已有的 key(例如 `providers.anthropic`
|
|
164
|
+
只覆盖 `baseURL` / `apiKeyEnv`,用 `models:` 收窄到要用的几个 ID),模型 ID 因此命中目录条目,
|
|
165
|
+
自适应标记、`supportsTemperature` 等兼容位都会自动继承。代价是 provider ID 变了,
|
|
166
|
+
老会话里记录的路由名会对不上,所以通常仍是逐个模型打勾更省事。
|
|
167
|
+
|
|
168
|
+
> **已知边界**:思考强度选 `off` 时,`dsh-llm-pi-ai` 会把声明里 `off: null` 的那一项**移出**
|
|
169
|
+
> `thinkingLevelMap`,pi-ai 因此发 `thinking: {type:"disabled"}`(只有把 `off` 钉成 `null` 才会
|
|
170
|
+
> 整个省略该参数——那是内置目录条目的写法),本开关管不到这一段。若网关连 `disabled` 也拒绝,
|
|
171
|
+
> 把该模型的「可选推理档位」全部取消(写成 `reasoningEfforts: false`,即视为无推理能力),
|
|
172
|
+
> 请求里就完全不会出现 `thinking`。
|
|
173
|
+
|
|
174
|
+
> **耦合点**:字段名 `forceAdaptiveThinking` 与「只在该协议的白名单内」都由上游决定
|
|
175
|
+
> ——`dsh-llm-pi-ai` 的 `anthropic-messages` compat 门(`0.1.7-alpha.2` 起已含该字段,
|
|
176
|
+
> `884f7b9c41` 于 2026-08-18 引入)会拒收白名单外的字段,pi-ai 的
|
|
177
|
+
> `dist/api/anthropic-messages.js` 是唯一读它的地方。`tests/adaptive-thinking.test.mjs`
|
|
178
|
+
> 在有 `DSH_TEST_DEPLOY_ROOT` 时直接问该部署的 pi-ai 要请求体,上游改名会在这里失败而不是在会话里变成 400。
|
|
179
|
+
|
|
180
|
+
## dsh 0.2.1(alpha)适配(v2.1)
|
|
181
|
+
|
|
182
|
+
- **标题 helper 换了 API。** 0.2.1-alpha 系列删掉了 `generateSessionTitleWithLlm`,改成
|
|
183
|
+
`executeSessionTitleLlm(ctx, config, request, providerId, prepared)`:提示词、输入包装、推理档位选择
|
|
184
|
+
与输出解析都交还给提供方。本插件按宿主实际导出择一:有新 API 就走新 API(提示词与解析规则复刻官方
|
|
185
|
+
`session-title-first-prompt-llm`,推理档位取路由最低档),否则回落旧 helper,0.1.7–0.2.0 不受影响。
|
|
186
|
+
2.0.x 在 0.2.1 上会因为找不到旧导出而整行激活失败(绘图与设置页一起没了)。
|
|
187
|
+
- **标题注册失败不再拖垮整个插件。** `apply` 里标题路由单独兜底:失败只打一条 warn、不注册标题提供方,
|
|
188
|
+
绘图工具与设置页照常。
|
|
189
|
+
- **与官方「会话标题刷新」(可选目录里的 `session-titles-bundle`)互斥。** 两者都要独占
|
|
190
|
+
`ctx.sessionTitle.register()`,后注册的一方失败——本插件这边表现为上面那条 warn。二选一。
|
|
191
|
+
- **推理档位与官方 Models 页写的是同一个字段。** 0.2.1-alpha.4 起官方 Models 页可以给手工声明的 pi-ai
|
|
192
|
+
模型编辑 `reasoningEfforts`,并允许每一档发送自定义 API 值(如 `high → deep`)。本页勾选档位时保留
|
|
193
|
+
已存的值,只有新加的档位才默认用档位名;`off` 的已存值也原样保留。
|
|
194
|
+
|
|
139
195
|
## 与 DSH 版本的耦合点(0.1.7-alpha.2)
|
|
140
196
|
|
|
141
197
|
- **provider 目录必须取 `llm.listConfigurableProviders()`**(返回 `LlmConfigurableProvider`
|
|
@@ -155,15 +211,20 @@ node --test tests/title-route.test.mjs # 标题路由解析与 Config
|
|
|
155
211
|
node --test tests/title-purpose.test.mjs # 用途页读写规划 + locale 回归
|
|
156
212
|
node --test tests/image-link.test.mjs # 生图链接写入规划(纯函数)
|
|
157
213
|
node --test tests/image.test.mjs # 绘图传输/工具/Remote(本地 HTTP,不外联)
|
|
214
|
+
node --test tests/adaptive-thinking.test.mjs # 思考格式开关:白名单 + 写入规划(+ 部署耦合守卫)
|
|
158
215
|
DSH_TEST_DEPLOY_ROOT=<隔离部署> node --test tests/title-provider.test.mjs # 真 helper + 假 ctx
|
|
159
216
|
```
|
|
160
217
|
|
|
161
218
|
- `title-provider.test.mjs` 用**假 ctx + 真共享策略**(只 mock `ctx.llm.stream`)验证唯一注册、
|
|
162
219
|
继承/指定路由、首条消息选择、辅助请求记录、失败与取消路径。`tests/_host.mjs` 要求
|
|
163
220
|
`DSH_TEST_DEPLOY_ROOT` 指向隔离的 0.1.7-alpha.2 部署。
|
|
221
|
+
- `adaptive-thinking.test.mjs` 的前半段是纯函数(开关只出现在 `anthropic-messages` 路由、
|
|
222
|
+
写入落在模型条目的 `compat` 上,目录路由落到 `modelOverrides`);后半段只在有
|
|
223
|
+
`DSH_TEST_DEPLOY_ROOT` 时运行,把该部署的 pi-ai 拦在假 `fetch` 前,直接读出请求体,
|
|
224
|
+
证明「打勾 → `thinking.type=adaptive` + `output_config.effort`,不打勾 → 预算式」。
|
|
164
225
|
- **这些测试不是实机验证**。实机验收:标题从会话日志读 `session/title-llm-request` 的 `route`
|
|
165
226
|
与最终 `session/title` 的 `source.model`;绘图确认「模型参数」勾选 → 绘图页出现条目 →
|
|
166
|
-
|
|
227
|
+
直接绘图面板出图;思考格式在模型下拉里选一档后看会话日志里那次请求的请求体。
|
|
167
228
|
|
|
168
229
|
## 安装 / 卸载
|
|
169
230
|
|
package/client.js
CHANGED
|
@@ -142,6 +142,27 @@ window.__ModuleLoader__.load({
|
|
|
142
142
|
const MODALITY_CHOICES = ["auto", "text", "text-image"];
|
|
143
143
|
const MODALITY_LABELS = { auto: "自动", text: "纯文本", "text-image": "文本 + 图像" };
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The compat switch that selects Anthropic's adaptive thinking request.
|
|
147
|
+
*
|
|
148
|
+
* pi-ai sends `thinking: {type:"adaptive"}` plus `output_config.effort` only
|
|
149
|
+
* for a model whose `compat.forceAdaptiveThinking` is true; without it the
|
|
150
|
+
* request carries the legacy budget form (`thinking.type="enabled"` with
|
|
151
|
+
* `budget_tokens`). A model that requires adaptive thinking answers that
|
|
152
|
+
* legacy request with HTTP 400 — "requires adaptive thinking; omit thinking
|
|
153
|
+
* or use thinking.type=adaptive and output_config.effort". The installed
|
|
154
|
+
* catalog states the flag for the model ids it ships; a route this profile
|
|
155
|
+
* declares by hand has no catalog entry to inherit it from, so the switch
|
|
156
|
+
* has to be written on the model entry here.
|
|
157
|
+
*/
|
|
158
|
+
const ADAPTIVE_THINKING_COMPAT_KEY = "forceAdaptiveThinking";
|
|
159
|
+
/**
|
|
160
|
+
* The one wire protocol whose compat takes that switch. `llm-pi-ai` refuses
|
|
161
|
+
* a compat field the model's resolved api does not offer, so the control is
|
|
162
|
+
* shown only on a route whose own configuration names this protocol.
|
|
163
|
+
*/
|
|
164
|
+
const ADAPTIVE_THINKING_API = "anthropic-messages";
|
|
165
|
+
|
|
145
166
|
function formatCapacity(n) {
|
|
146
167
|
if (n == null || typeof n !== "number" || !Number.isFinite(n)) return "";
|
|
147
168
|
if (n % 1000000 === 0) return String(n / 1000000) + "M";
|
|
@@ -327,6 +348,25 @@ window.__ModuleLoader__.load({
|
|
|
327
348
|
return undefined;
|
|
328
349
|
}
|
|
329
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The compat key this page may write for one route, or null when the route
|
|
353
|
+
* cannot take it.
|
|
354
|
+
*
|
|
355
|
+
* The route's own `api` is the only place the protocol is known before a
|
|
356
|
+
* model resolves: a hand-declared route spells it out, while a catalog route
|
|
357
|
+
* omits it and resolves each model's protocol from the installed catalog.
|
|
358
|
+
* The control therefore appears exactly where the switch is both needed (no
|
|
359
|
+
* catalog entry to inherit the flag from) and accepted.
|
|
360
|
+
* @param settingsNs - the provider's settings namespace.
|
|
361
|
+
* @param profile - the route's own settings section.
|
|
362
|
+
* @returns the adaptive-thinking compat key, or null.
|
|
363
|
+
*/
|
|
364
|
+
function adaptiveThinkingCompatKey(settingsNs, profile) {
|
|
365
|
+
if (settingsNs !== "llm-pi-ai") return null;
|
|
366
|
+
if (!profile || typeof profile !== "object") return null;
|
|
367
|
+
return profile.api === ADAPTIVE_THINKING_API ? ADAPTIVE_THINKING_COMPAT_KEY : null;
|
|
368
|
+
}
|
|
369
|
+
|
|
330
370
|
function computeWriteOps(entry, nsView, modelId, field, value) {
|
|
331
371
|
const ns = entry.settingsNs;
|
|
332
372
|
const settingsPath = entry.settingsPath || [];
|
|
@@ -365,6 +405,44 @@ window.__ModuleLoader__.load({
|
|
|
365
405
|
return { ns, ops: [{ op: "set", path: [...basePath, "models"], value: [{ id: modelId, [field]: value }] }] };
|
|
366
406
|
}
|
|
367
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The stored `reasoningEfforts` of one model: its `models[]` row first,
|
|
410
|
+
* then (pi-ai) its `modelOverrides` entry; undefined when neither declares it.
|
|
411
|
+
*/
|
|
412
|
+
function storedReasoningEfforts(entry, nsView, modelId) {
|
|
413
|
+
const profile = at(nsView ? nsView.value : undefined, entry.settingsPath || []);
|
|
414
|
+
const models = (profile && Array.isArray(profile.models)) ? profile.models : [];
|
|
415
|
+
const row = models.find((m) => m && typeof m === "object" && m.id === modelId);
|
|
416
|
+
if (row && row.reasoningEfforts !== undefined) return row.reasoningEfforts;
|
|
417
|
+
const overrides = profile && profile.modelOverrides && typeof profile.modelOverrides === "object"
|
|
418
|
+
? profile.modelOverrides[modelId] : undefined;
|
|
419
|
+
return overrides && typeof overrides === "object" ? overrides.reasoningEfforts : undefined;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The `reasoningEfforts` value for a set of checked levels.
|
|
424
|
+
*
|
|
425
|
+
* Since dsh 0.2.1-alpha.4 the official Models page also edits this field and
|
|
426
|
+
* lets each level send its own API value (a gateway spelling `high` as
|
|
427
|
+
* `deep`). Toggling a level here keeps every value already stored for the
|
|
428
|
+
* levels that stay, including a non-null `off`; only a newly added level
|
|
429
|
+
* defaults to its own name.
|
|
430
|
+
* @param levels - checked levels, in any order.
|
|
431
|
+
* @param existing - the stored value, or undefined.
|
|
432
|
+
* @returns `false` for no reasoning, else the level → wire-value dict.
|
|
433
|
+
*/
|
|
434
|
+
function reasoningEffortsPayload(levels, existing) {
|
|
435
|
+
const ordered = REASONING_LEVELS.filter((level) => levels.indexOf(level) >= 0);
|
|
436
|
+
if (ordered.length === 0) return false;
|
|
437
|
+
const stored = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
|
|
438
|
+
const dict = { off: stored.off !== undefined ? stored.off : null };
|
|
439
|
+
for (const level of ordered) {
|
|
440
|
+
const wire = stored[level];
|
|
441
|
+
dict[level] = typeof wire === "string" && wire !== "" ? wire : level;
|
|
442
|
+
}
|
|
443
|
+
return dict;
|
|
444
|
+
}
|
|
445
|
+
|
|
368
446
|
function computeCompatWriteOps(entry, nsView, modelId, compatKey, value) {
|
|
369
447
|
const ns = entry.settingsNs;
|
|
370
448
|
const settingsPath = entry.settingsPath || [];
|
|
@@ -895,6 +973,7 @@ window.__ModuleLoader__.load({
|
|
|
895
973
|
const nsView = entry ? nsByNs.get(entry.settingsNs) : undefined;
|
|
896
974
|
const providerProfile = profileOf(nsView, entry ? entry.settingsPath : []);
|
|
897
975
|
const reasoningEditable = !!(entry && entry.settingsNs === "llm-pi-ai");
|
|
976
|
+
const adaptiveKey = entry ? adaptiveThinkingCompatKey(entry.settingsNs, providerProfile) : null;
|
|
898
977
|
const routeDefaultInput = providerProfile && Array.isArray(providerProfile.defaultInput)
|
|
899
978
|
? providerProfile.defaultInput
|
|
900
979
|
: null;
|
|
@@ -904,11 +983,15 @@ window.__ModuleLoader__.load({
|
|
|
904
983
|
const devRole = entry && entry.settingsNs === "llm-pi-ai"
|
|
905
984
|
? readCompatField(nsView, entry.settingsPath, m.id, "supportsDeveloperRole")
|
|
906
985
|
: undefined;
|
|
986
|
+
const adaptiveThinking = adaptiveKey
|
|
987
|
+
? readCompatField(nsView, entry.settingsPath, m.id, adaptiveKey)
|
|
988
|
+
: undefined;
|
|
907
989
|
const modalityField = entry ? modalityFieldFor(entry.settingsNs) : undefined;
|
|
908
990
|
const rawInput = modalityField
|
|
909
991
|
? readModelField(nsView, entry.settingsPath, m.id, modalityField)
|
|
910
992
|
: undefined;
|
|
911
993
|
const image = computeImageModelState(view, providerProfile, m.id);
|
|
994
|
+
const efforts = (m.reasoning && Array.isArray(m.reasoning.efforts)) ? m.reasoning.efforts.map((e) => ({ id: e.id, name: e.name })) : [];
|
|
912
995
|
return {
|
|
913
996
|
id: m.id,
|
|
914
997
|
name: m.name,
|
|
@@ -916,6 +999,10 @@ window.__ModuleLoader__.load({
|
|
|
916
999
|
contextWindow: ctx != null ? ctx : null,
|
|
917
1000
|
maxTokens: mx != null ? mx : null,
|
|
918
1001
|
supportsDeveloperRole: devRole,
|
|
1002
|
+
adaptiveThinking: adaptiveThinking === true,
|
|
1003
|
+
// The flag only changes a request that carries thinking at all,
|
|
1004
|
+
// so it is offered where a level can be selected.
|
|
1005
|
+
adaptiveThinkingEditable: adaptiveKey !== null && efforts.length > 0,
|
|
919
1006
|
modalityField: modalityField || null,
|
|
920
1007
|
modality: modalityField ? modalityChoice(rawInput) : null,
|
|
921
1008
|
imageAvailable: image.available,
|
|
@@ -925,7 +1012,7 @@ window.__ModuleLoader__.load({
|
|
|
925
1012
|
imageApiKeyEnv: image.apiKeyEnv || null,
|
|
926
1013
|
declared: !!(entry && entry.declared),
|
|
927
1014
|
routeDefaultInput: routeDefaultInput,
|
|
928
|
-
efforts
|
|
1015
|
+
efforts,
|
|
929
1016
|
defaultEffort: (m.reasoning && m.reasoning.defaultEffort != null) ? m.reasoning.defaultEffort : null,
|
|
930
1017
|
};
|
|
931
1018
|
});
|
|
@@ -1010,25 +1097,19 @@ window.__ModuleLoader__.load({
|
|
|
1010
1097
|
if (kind === "reasoningLevels") {
|
|
1011
1098
|
field = "reasoningEfforts";
|
|
1012
1099
|
const raw = value && Array.isArray(value.levels) ? value.levels : [];
|
|
1013
|
-
|
|
1014
|
-
if (levels.length === 0) {
|
|
1015
|
-
payload = false;
|
|
1016
|
-
} else {
|
|
1017
|
-
const dict = { off: null };
|
|
1018
|
-
for (const level of levels) dict[level] = level;
|
|
1019
|
-
payload = dict;
|
|
1020
|
-
}
|
|
1100
|
+
payload = reasoningEffortsPayload(raw, storedReasoningEfforts(entry, nsView, model));
|
|
1021
1101
|
} else if (kind === "contextWindow" || kind === "maxTokens") {
|
|
1022
1102
|
field = kind;
|
|
1023
1103
|
if (value != null && (typeof value !== "number" || !Number.isInteger(value) || value <= 0)) {
|
|
1024
1104
|
throw new Error("取值必须是正整数");
|
|
1025
1105
|
}
|
|
1026
1106
|
payload = value;
|
|
1027
|
-
} else if (kind === "supportsDeveloperRole") {
|
|
1107
|
+
} else if (kind === "supportsDeveloperRole" || kind === "adaptiveThinking") {
|
|
1028
1108
|
if (value !== null && typeof value !== "boolean") {
|
|
1029
1109
|
throw new Error("取值必须是布尔值");
|
|
1030
1110
|
}
|
|
1031
|
-
const
|
|
1111
|
+
const compatKey = kind === "supportsDeveloperRole" ? "supportsDeveloperRole" : ADAPTIVE_THINKING_COMPAT_KEY;
|
|
1112
|
+
const write = computeCompatWriteOps(entry, nsView, model, compatKey, value);
|
|
1032
1113
|
if (write) {
|
|
1033
1114
|
const res = await api.settings.mutate({ ns: write.ns, ops: write.ops });
|
|
1034
1115
|
if (!res.result.ok) throw new Error(res.result.error.message);
|
|
@@ -1176,6 +1257,9 @@ window.__ModuleLoader__.load({
|
|
|
1176
1257
|
: null,
|
|
1177
1258
|
model.imageLinked
|
|
1178
1259
|
? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "生图模型")
|
|
1260
|
+
: null,
|
|
1261
|
+
model.adaptiveThinking
|
|
1262
|
+
? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "自适应思考")
|
|
1179
1263
|
: null
|
|
1180
1264
|
)
|
|
1181
1265
|
),
|
|
@@ -1215,6 +1299,7 @@ window.__ModuleLoader__.load({
|
|
|
1215
1299
|
React.createElement("input", {
|
|
1216
1300
|
type: "radio",
|
|
1217
1301
|
name: "devrole-" + model.id,
|
|
1302
|
+
disabled: !editable,
|
|
1218
1303
|
checked: role === "auto"
|
|
1219
1304
|
? model.supportsDeveloperRole === undefined
|
|
1220
1305
|
: (role === "developer" ? model.supportsDeveloperRole === true : model.supportsDeveloperRole === false),
|
|
@@ -1236,6 +1321,29 @@ window.__ModuleLoader__.load({
|
|
|
1236
1321
|
"自动:跟随网关检测;如遇 role 'developer' is not allowed,请选 system"
|
|
1237
1322
|
)
|
|
1238
1323
|
) : null,
|
|
1324
|
+
model.adaptiveThinkingEditable ? React.createElement("div", { className: "mcfg-reasoning" },
|
|
1325
|
+
React.createElement("span", { className: "mcfg-label" }, "思考格式(Anthropic 兼容开关)"),
|
|
1326
|
+
React.createElement("div", { className: "mcfg-levels" },
|
|
1327
|
+
["auto", "adaptive"].map((choice) =>
|
|
1328
|
+
React.createElement("label", { key: choice, className: "mcfg-level" },
|
|
1329
|
+
React.createElement("input", {
|
|
1330
|
+
type: "radio",
|
|
1331
|
+
name: "adaptive-" + model.id,
|
|
1332
|
+
disabled: !editable,
|
|
1333
|
+
checked: choice === "auto" ? !model.adaptiveThinking : model.adaptiveThinking,
|
|
1334
|
+
onChange: () => applyChange(provider.provider, model.id, "adaptiveThinking", choice === "adaptive" ? true : null),
|
|
1335
|
+
}),
|
|
1336
|
+
React.createElement("span", null, choice === "auto" ? "自动(预算式 thinking)" : "自适应 thinking")
|
|
1337
|
+
)
|
|
1338
|
+
)
|
|
1339
|
+
),
|
|
1340
|
+
React.createElement("div", { className: "mcfg-note" },
|
|
1341
|
+
"上面选中档位后,请求体由这里决定:自适应写 compat." + ADAPTIVE_THINKING_COMPAT_KEY
|
|
1342
|
+
+ ",发出 thinking.type=adaptive 与 output_config.effort;Claude Opus 4.6 及更新、Sonnet 5、"
|
|
1343
|
+
+ "Fable 5 等只接受这种,报 400 requires adaptive thinking 就切到它。"
|
|
1344
|
+
+ "Claude 4.5 及更早只认预算式,保持「自动」。"
|
|
1345
|
+
)
|
|
1346
|
+
) : null,
|
|
1239
1347
|
model.modalityField ? React.createElement("div", { className: "mcfg-reasoning" },
|
|
1240
1348
|
React.createElement("span", { className: "mcfg-label" }, "输入模态(写入 " + model.modalityField + ")"),
|
|
1241
1349
|
React.createElement("div", { className: "mcfg-levels" },
|
package/index.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* patch in `cordis.patch.yml`. It is still exactly one provider:
|
|
11
11
|
* `ctx.sessionTitle.register()` throws on a second registration, so a
|
|
12
12
|
* competing generator cannot coexist. Everything except the route is
|
|
13
|
-
* delegated to the public shared policy `
|
|
13
|
+
* delegated to the public shared policy (`executeSessionTitleLlm`, or
|
|
14
|
+
* `generateSessionTitleWithLlm` before dsh 0.2.1) from
|
|
14
15
|
* `@deepseek-ai/dsh-session-title-llm`, resolved from the RUNNING
|
|
15
16
|
* deployment (see hostRequire) — never from this package's directory, which
|
|
16
17
|
* for a linked workspace package would risk a second copy of the host's
|
|
@@ -140,7 +141,79 @@ function hostRequire() {
|
|
|
140
141
|
return undefined
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
/** Title-service package that owns `normalizeSessionTitle`; resolved next to the helper. */
|
|
145
|
+
const TITLE_SERVICE_PACKAGE = '@deepseek-ai/dsh-session-title'
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Language-aware system instruction, mirroring the shipped first-prompt
|
|
149
|
+
* provider (dsh ≥ 0.2.1 hands prompt ownership to each provider).
|
|
150
|
+
* @param budget - the live title budget.
|
|
151
|
+
* @returns the system prompt.
|
|
152
|
+
*/
|
|
153
|
+
export function titleSystemPrompt(budget) {
|
|
154
|
+
return [
|
|
155
|
+
'Create a concise title for an AI coding-assistant session from the supplied human messages.',
|
|
156
|
+
'Return only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.',
|
|
157
|
+
'Use the language of the messages.',
|
|
158
|
+
`Aim for about ${budget.targetWords} words in non-CJK languages or ${budget.targetCjkCharacters} CJK characters.`,
|
|
159
|
+
'If the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.',
|
|
160
|
+
].join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Asterisk emphasis wrapping a whole line, such as `**Title**`. */
|
|
164
|
+
const EMPHASIS_WRAPPER = /^(?<marker>\*{1,3})(?<inner>\S(?:.*\S)?)\k<marker>$/u
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Take the title from model output: the first non-empty line, without
|
|
168
|
+
* asterisk emphasis wrapping that whole line (same rule as the shipped provider).
|
|
169
|
+
* @param text - the joined text blocks.
|
|
170
|
+
* @returns the raw title line.
|
|
171
|
+
*/
|
|
172
|
+
export function titleFromOutput(text) {
|
|
173
|
+
const line = text.split(/\r?\n/u).map(item => item.trim()).find(item => item.length > 0) ?? ''
|
|
174
|
+
const groups = EMPHASIS_WRAPPER.exec(line)?.groups
|
|
175
|
+
const marker = groups?.marker
|
|
176
|
+
const inner = groups?.inner
|
|
177
|
+
return marker === undefined || inner === undefined || inner.includes(marker) ? line : inner
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Adapt the prepared-request API (`executeSessionTitleLlm`, dsh ≥ 0.2.1) to the
|
|
182
|
+
* one-call shape this plugin drives.
|
|
183
|
+
* @param execute - the host's `executeSessionTitleLlm`.
|
|
184
|
+
* @param normalize - the host's `normalizeSessionTitle`.
|
|
185
|
+
* @returns `(ctx, budget, request, selected, providerId) => { title, messageSeqs, model }`.
|
|
186
|
+
*/
|
|
187
|
+
export function preparedTitleGenerator(execute, normalize) {
|
|
188
|
+
return async (ctx, budget, request, selected, providerId) => {
|
|
189
|
+
const config = { maxInputBytes: budget.maxInputBytes, maxOutputTokens: budget.maxOutputTokens, timeoutMs: budget.timeoutMs }
|
|
190
|
+
const messageSeqs = selected.map(message => message.seq)
|
|
191
|
+
const response = await execute(ctx, config, request, providerId, {
|
|
192
|
+
system: titleSystemPrompt(budget),
|
|
193
|
+
input: `Generate the session title from this JSON array of human messages:\n${JSON.stringify(selected)}`,
|
|
194
|
+
messageSeqs,
|
|
195
|
+
// A title needs no deliberation: take the route's least effort.
|
|
196
|
+
selectReasoningEffort: model => model.reasoning?.efforts?.[0]?.id,
|
|
197
|
+
})
|
|
198
|
+
if (response.finish.kind === 'tool-calls' || response.blocks.some(block => block.type === 'tool-call')) {
|
|
199
|
+
throw new Error('session-title-llm: title output must contain text only')
|
|
200
|
+
}
|
|
201
|
+
if (response.finish.kind === 'max-tokens') {
|
|
202
|
+
throw new Error('session-title-llm: title output reached maxOutputTokens')
|
|
203
|
+
}
|
|
204
|
+
const text = response.blocks.filter(block => block.type === 'text').map(block => block.text).join(' ')
|
|
205
|
+
const title = normalize(titleFromOutput(text), Number.MAX_SAFE_INTEGER)
|
|
206
|
+
if (title.length === 0) throw new Error('session-title-llm: title model produced no text')
|
|
207
|
+
return { title, messageSeqs, model: response.model }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
143
211
|
let helperPromise
|
|
212
|
+
/**
|
|
213
|
+
* Load a title generator from the running deployment: the prepared-request
|
|
214
|
+
* API when the host has it (≥ 0.2.1), else the one-call
|
|
215
|
+
* `generateSessionTitleWithLlm` (0.1.7 – 0.2.0).
|
|
216
|
+
*/
|
|
144
217
|
function loadTitleHelper() {
|
|
145
218
|
helperPromise ??= (async () => {
|
|
146
219
|
const require = hostRequire()
|
|
@@ -149,12 +222,17 @@ function loadTitleHelper() {
|
|
|
149
222
|
`model-tuning: 无法从宿主解析 ${HELPER_PACKAGE};本插件必须与提供该包的部署一起运行。`,
|
|
150
223
|
)
|
|
151
224
|
}
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
if (typeof
|
|
155
|
-
|
|
225
|
+
const helperPath = require.resolve(HELPER_PACKAGE)
|
|
226
|
+
const loaded = await import(pathToFileURL(helperPath).href)
|
|
227
|
+
if (typeof loaded.executeSessionTitleLlm === 'function') {
|
|
228
|
+
const service = await import(pathToFileURL(createRequire(helperPath).resolve(TITLE_SERVICE_PACKAGE)).href)
|
|
229
|
+
if (typeof service.normalizeSessionTitle !== 'function') {
|
|
230
|
+
throw new Error(`model-tuning: ${TITLE_SERVICE_PACKAGE} 未导出 normalizeSessionTitle`)
|
|
231
|
+
}
|
|
232
|
+
return preparedTitleGenerator(loaded.executeSessionTitleLlm, service.normalizeSessionTitle)
|
|
156
233
|
}
|
|
157
|
-
return
|
|
234
|
+
if (typeof loaded.generateSessionTitleWithLlm === 'function') return loaded.generateSessionTitleWithLlm
|
|
235
|
+
throw new Error(`model-tuning: ${HELPER_PACKAGE} 既没有 executeSessionTitleLlm 也没有 generateSessionTitleWithLlm`)
|
|
158
236
|
})()
|
|
159
237
|
return helperPromise
|
|
160
238
|
}
|
|
@@ -189,7 +267,7 @@ export function titleBudgetOf(config) {
|
|
|
189
267
|
* @param config - the live Config (route and budget are re-read per generation).
|
|
190
268
|
*/
|
|
191
269
|
export async function registerTitleProvider(ctx, config) {
|
|
192
|
-
const
|
|
270
|
+
const generateTitle = await loadTitleHelper()
|
|
193
271
|
validateTitleModel({ titleMode: config.titleMode.get(), titleProvider: config.titleProvider.get(), titleModel: config.titleModel.get() })
|
|
194
272
|
titleBudgetOf(config)
|
|
195
273
|
ctx.sessionTitle.register({
|
|
@@ -200,7 +278,7 @@ export async function registerTitleProvider(ctx, config) {
|
|
|
200
278
|
titleMode: config.titleMode.get(), titleProvider: config.titleProvider.get(), titleModel: config.titleModel.get(),
|
|
201
279
|
}, request)
|
|
202
280
|
const selected = request.messages.slice(0, 1)
|
|
203
|
-
return await
|
|
281
|
+
return await generateTitle(ctx, titleBudgetOf(config), { ...request, route }, selected, TITLE_PROVIDER_ID)
|
|
204
282
|
},
|
|
205
283
|
})
|
|
206
284
|
}
|
|
@@ -361,6 +439,13 @@ export function registerImageTools(ctx, config) {
|
|
|
361
439
|
* @param config - the unified Config described above.
|
|
362
440
|
*/
|
|
363
441
|
export async function apply(ctx, config) {
|
|
364
|
-
|
|
442
|
+
// The title route leans on a host helper whose API has changed between
|
|
443
|
+
// releases. If it breaks again, lose only title routing — never the image
|
|
444
|
+
// tools and settings page that ride on the same row.
|
|
445
|
+
try {
|
|
446
|
+
await registerTitleProvider(ctx, config)
|
|
447
|
+
} catch (error) {
|
|
448
|
+
ctx.logger?.warn?.(`model-tuning: 标题路由未启用(${error instanceof Error ? error.message : String(error)});绘图与设置页照常工作`)
|
|
449
|
+
}
|
|
365
450
|
registerImageTools(ctx, config)
|
|
366
451
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-model-tuning",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"keywords": ["dsh-plugin", "deepseek-harness", "dsh", "model", "settings", "reasoning", "context", "session-title", "image-generation"],
|
|
6
6
|
"publishConfig": { "registry": "https://registry.npmjs.org" },
|