dsh-plugin-model-tuning 2.0.2 → 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 +15 -0
- package/client.js +39 -8
- 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
|
@@ -177,6 +177,21 @@ thinking.type=adaptive and output_config.effort","type":"invalid_request_error"}
|
|
|
177
177
|
> `dist/api/anthropic-messages.js` 是唯一读它的地方。`tests/adaptive-thinking.test.mjs`
|
|
178
178
|
> 在有 `DSH_TEST_DEPLOY_ROOT` 时直接问该部署的 pi-ai 要请求体,上游改名会在这里失败而不是在会话里变成 400。
|
|
179
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
|
+
|
|
180
195
|
## 与 DSH 版本的耦合点(0.1.7-alpha.2)
|
|
181
196
|
|
|
182
197
|
- **provider 目录必须取 `llm.listConfigurableProviders()`**(返回 `LlmConfigurableProvider`
|
package/client.js
CHANGED
|
@@ -405,6 +405,44 @@ window.__ModuleLoader__.load({
|
|
|
405
405
|
return { ns, ops: [{ op: "set", path: [...basePath, "models"], value: [{ id: modelId, [field]: value }] }] };
|
|
406
406
|
}
|
|
407
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
|
+
|
|
408
446
|
function computeCompatWriteOps(entry, nsView, modelId, compatKey, value) {
|
|
409
447
|
const ns = entry.settingsNs;
|
|
410
448
|
const settingsPath = entry.settingsPath || [];
|
|
@@ -1059,14 +1097,7 @@ window.__ModuleLoader__.load({
|
|
|
1059
1097
|
if (kind === "reasoningLevels") {
|
|
1060
1098
|
field = "reasoningEfforts";
|
|
1061
1099
|
const raw = value && Array.isArray(value.levels) ? value.levels : [];
|
|
1062
|
-
|
|
1063
|
-
if (levels.length === 0) {
|
|
1064
|
-
payload = false;
|
|
1065
|
-
} else {
|
|
1066
|
-
const dict = { off: null };
|
|
1067
|
-
for (const level of levels) dict[level] = level;
|
|
1068
|
-
payload = dict;
|
|
1069
|
-
}
|
|
1100
|
+
payload = reasoningEffortsPayload(raw, storedReasoningEfforts(entry, nsView, model));
|
|
1070
1101
|
} else if (kind === "contextWindow" || kind === "maxTokens") {
|
|
1071
1102
|
field = kind;
|
|
1072
1103
|
if (value != null && (typeof value !== "number" || !Number.isInteger(value) || value <= 0)) {
|
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" },
|