dsh-plugin-model-tuning 2.0.1 → 2.0.2

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.
Files changed (3) hide show
  1. package/README.md +47 -1
  2. package/client.js +80 -3
  3. package/package.json +1 -1
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,44 @@ 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
+
139
180
  ## 与 DSH 版本的耦合点(0.1.7-alpha.2)
140
181
 
141
182
  - **provider 目录必须取 `llm.listConfigurableProviders()`**(返回 `LlmConfigurableProvider`
@@ -155,15 +196,20 @@ node --test tests/title-route.test.mjs # 标题路由解析与 Config
155
196
  node --test tests/title-purpose.test.mjs # 用途页读写规划 + locale 回归
156
197
  node --test tests/image-link.test.mjs # 生图链接写入规划(纯函数)
157
198
  node --test tests/image.test.mjs # 绘图传输/工具/Remote(本地 HTTP,不外联)
199
+ node --test tests/adaptive-thinking.test.mjs # 思考格式开关:白名单 + 写入规划(+ 部署耦合守卫)
158
200
  DSH_TEST_DEPLOY_ROOT=<隔离部署> node --test tests/title-provider.test.mjs # 真 helper + 假 ctx
159
201
  ```
160
202
 
161
203
  - `title-provider.test.mjs` 用**假 ctx + 真共享策略**(只 mock `ctx.llm.stream`)验证唯一注册、
162
204
  继承/指定路由、首条消息选择、辅助请求记录、失败与取消路径。`tests/_host.mjs` 要求
163
205
  `DSH_TEST_DEPLOY_ROOT` 指向隔离的 0.1.7-alpha.2 部署。
206
+ - `adaptive-thinking.test.mjs` 的前半段是纯函数(开关只出现在 `anthropic-messages` 路由、
207
+ 写入落在模型条目的 `compat` 上,目录路由落到 `modelOverrides`);后半段只在有
208
+ `DSH_TEST_DEPLOY_ROOT` 时运行,把该部署的 pi-ai 拦在假 `fetch` 前,直接读出请求体,
209
+ 证明「打勾 → `thinking.type=adaptive` + `output_config.effort`,不打勾 → 预算式」。
164
210
  - **这些测试不是实机验证**。实机验收:标题从会话日志读 `session/title-llm-request` 的 `route`
165
211
  与最终 `session/title` 的 `source.model`;绘图确认「模型参数」勾选 → 绘图页出现条目 →
166
- 直接绘图面板出图。
212
+ 直接绘图面板出图;思考格式在模型下拉里选一档后看会话日志里那次请求的请求体。
167
213
 
168
214
  ## 安装 / 卸载
169
215
 
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 || [];
@@ -895,6 +935,7 @@ window.__ModuleLoader__.load({
895
935
  const nsView = entry ? nsByNs.get(entry.settingsNs) : undefined;
896
936
  const providerProfile = profileOf(nsView, entry ? entry.settingsPath : []);
897
937
  const reasoningEditable = !!(entry && entry.settingsNs === "llm-pi-ai");
938
+ const adaptiveKey = entry ? adaptiveThinkingCompatKey(entry.settingsNs, providerProfile) : null;
898
939
  const routeDefaultInput = providerProfile && Array.isArray(providerProfile.defaultInput)
899
940
  ? providerProfile.defaultInput
900
941
  : null;
@@ -904,11 +945,15 @@ window.__ModuleLoader__.load({
904
945
  const devRole = entry && entry.settingsNs === "llm-pi-ai"
905
946
  ? readCompatField(nsView, entry.settingsPath, m.id, "supportsDeveloperRole")
906
947
  : undefined;
948
+ const adaptiveThinking = adaptiveKey
949
+ ? readCompatField(nsView, entry.settingsPath, m.id, adaptiveKey)
950
+ : undefined;
907
951
  const modalityField = entry ? modalityFieldFor(entry.settingsNs) : undefined;
908
952
  const rawInput = modalityField
909
953
  ? readModelField(nsView, entry.settingsPath, m.id, modalityField)
910
954
  : undefined;
911
955
  const image = computeImageModelState(view, providerProfile, m.id);
956
+ const efforts = (m.reasoning && Array.isArray(m.reasoning.efforts)) ? m.reasoning.efforts.map((e) => ({ id: e.id, name: e.name })) : [];
912
957
  return {
913
958
  id: m.id,
914
959
  name: m.name,
@@ -916,6 +961,10 @@ window.__ModuleLoader__.load({
916
961
  contextWindow: ctx != null ? ctx : null,
917
962
  maxTokens: mx != null ? mx : null,
918
963
  supportsDeveloperRole: devRole,
964
+ adaptiveThinking: adaptiveThinking === true,
965
+ // The flag only changes a request that carries thinking at all,
966
+ // so it is offered where a level can be selected.
967
+ adaptiveThinkingEditable: adaptiveKey !== null && efforts.length > 0,
919
968
  modalityField: modalityField || null,
920
969
  modality: modalityField ? modalityChoice(rawInput) : null,
921
970
  imageAvailable: image.available,
@@ -925,7 +974,7 @@ window.__ModuleLoader__.load({
925
974
  imageApiKeyEnv: image.apiKeyEnv || null,
926
975
  declared: !!(entry && entry.declared),
927
976
  routeDefaultInput: routeDefaultInput,
928
- efforts: (m.reasoning && Array.isArray(m.reasoning.efforts)) ? m.reasoning.efforts.map((e) => ({ id: e.id, name: e.name })) : [],
977
+ efforts,
929
978
  defaultEffort: (m.reasoning && m.reasoning.defaultEffort != null) ? m.reasoning.defaultEffort : null,
930
979
  };
931
980
  });
@@ -1024,11 +1073,12 @@ window.__ModuleLoader__.load({
1024
1073
  throw new Error("取值必须是正整数");
1025
1074
  }
1026
1075
  payload = value;
1027
- } else if (kind === "supportsDeveloperRole") {
1076
+ } else if (kind === "supportsDeveloperRole" || kind === "adaptiveThinking") {
1028
1077
  if (value !== null && typeof value !== "boolean") {
1029
1078
  throw new Error("取值必须是布尔值");
1030
1079
  }
1031
- const write = computeCompatWriteOps(entry, nsView, model, "supportsDeveloperRole", value);
1080
+ const compatKey = kind === "supportsDeveloperRole" ? "supportsDeveloperRole" : ADAPTIVE_THINKING_COMPAT_KEY;
1081
+ const write = computeCompatWriteOps(entry, nsView, model, compatKey, value);
1032
1082
  if (write) {
1033
1083
  const res = await api.settings.mutate({ ns: write.ns, ops: write.ops });
1034
1084
  if (!res.result.ok) throw new Error(res.result.error.message);
@@ -1176,6 +1226,9 @@ window.__ModuleLoader__.load({
1176
1226
  : null,
1177
1227
  model.imageLinked
1178
1228
  ? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "生图模型")
1229
+ : null,
1230
+ model.adaptiveThinking
1231
+ ? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "自适应思考")
1179
1232
  : null
1180
1233
  )
1181
1234
  ),
@@ -1215,6 +1268,7 @@ window.__ModuleLoader__.load({
1215
1268
  React.createElement("input", {
1216
1269
  type: "radio",
1217
1270
  name: "devrole-" + model.id,
1271
+ disabled: !editable,
1218
1272
  checked: role === "auto"
1219
1273
  ? model.supportsDeveloperRole === undefined
1220
1274
  : (role === "developer" ? model.supportsDeveloperRole === true : model.supportsDeveloperRole === false),
@@ -1236,6 +1290,29 @@ window.__ModuleLoader__.load({
1236
1290
  "自动:跟随网关检测;如遇 role 'developer' is not allowed,请选 system"
1237
1291
  )
1238
1292
  ) : null,
1293
+ model.adaptiveThinkingEditable ? React.createElement("div", { className: "mcfg-reasoning" },
1294
+ React.createElement("span", { className: "mcfg-label" }, "思考格式(Anthropic 兼容开关)"),
1295
+ React.createElement("div", { className: "mcfg-levels" },
1296
+ ["auto", "adaptive"].map((choice) =>
1297
+ React.createElement("label", { key: choice, className: "mcfg-level" },
1298
+ React.createElement("input", {
1299
+ type: "radio",
1300
+ name: "adaptive-" + model.id,
1301
+ disabled: !editable,
1302
+ checked: choice === "auto" ? !model.adaptiveThinking : model.adaptiveThinking,
1303
+ onChange: () => applyChange(provider.provider, model.id, "adaptiveThinking", choice === "adaptive" ? true : null),
1304
+ }),
1305
+ React.createElement("span", null, choice === "auto" ? "自动(预算式 thinking)" : "自适应 thinking")
1306
+ )
1307
+ )
1308
+ ),
1309
+ React.createElement("div", { className: "mcfg-note" },
1310
+ "上面选中档位后,请求体由这里决定:自适应写 compat." + ADAPTIVE_THINKING_COMPAT_KEY
1311
+ + ",发出 thinking.type=adaptive 与 output_config.effort;Claude Opus 4.6 及更新、Sonnet 5、"
1312
+ + "Fable 5 等只接受这种,报 400 requires adaptive thinking 就切到它。"
1313
+ + "Claude 4.5 及更早只认预算式,保持「自动」。"
1314
+ )
1315
+ ) : null,
1239
1316
  model.modalityField ? React.createElement("div", { className: "mcfg-reasoning" },
1240
1317
  React.createElement("span", { className: "mcfg-label" }, "输入模态(写入 " + model.modalityField + ")"),
1241
1318
  React.createElement("div", { className: "mcfg-levels" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-model-tuning",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
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" },