dsh-tool-vision 0.9.2 → 0.9.3

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 CHANGED
@@ -382,6 +382,31 @@ the panel's picker carries a badge:
382
382
  > A route on an unknown protocol (e.g. `openai-responses`) returns `unknown`
383
383
  > with a reason, rather than guessing confidently with `chat/completions`.
384
384
 
385
+ ## v0.9.3: adapter snapshot model admission (pi-ai guard)
386
+
387
+ **The bug.** `v0.9.2`'s `installDispatchImageAdmission` satisfied DSH core's
388
+ `LlmService.generate` by declaring `image` on `adapterCall.model.inputModalities`.
389
+ However, `dsh-llm-pi-ai`'s adapter executes a second internal guard inside its
390
+ streaming path (`streamWithSnapshot`):
391
+
392
+ ```js
393
+ const model = this.modelOf(snapshot, options.provider, options.model);
394
+ if (containsImage && !model.input.includes("image"))
395
+ throw new LlmError(`pi-ai model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
396
+ ```
397
+
398
+ `this.modelOf` resolves from the adapter's own `snapshot.models` catalog, which
399
+ defaults to `input: ["text"]` unless the user explicitly declared
400
+ `input: [text, image]` in `settings.yaml`. When admitted by `v0.9.2`, the raw image
401
+ reached `streamWithSnapshot` and triggered `UNSUPPORTED_CONTENT`. Since the image
402
+ was already in the session's durable transcript, every subsequent turn failed.
403
+
404
+ **The fix.** On direct routes, `installDispatchImageAdmission` now additionally:
405
+ 1. Wraps `adapter.modelOf` (when present) to include `"image"` in `resolved.input`;
406
+ 2. Enriches the live snapshot model (`snapshot.models.getModel(route, model).input`)
407
+ with `"image"`;
408
+ 3. Fully restores `adapter.modelOf` and reverts modified input arrays upon dispose.
409
+
385
410
  ## v0.9.2: admission is not dispatch
386
411
 
387
412
  **The bug.** Every capability signal this plugin collects — `probeResults`,
package/README.zh.md CHANGED
@@ -244,6 +244,23 @@ base = autoDetect ? 路由声明含 image : ∅
244
244
 
245
245
  > 未知协议的路由(如 `openai-responses`)会直接返回 `unknown` 并说明原因,而不是用 `chat/completions` 去猜一个自信的错误答案。
246
246
 
247
+ ## v0.9.3:适配器快照模型准入(pi-ai 门禁放行)
248
+
249
+ **问题**。`v0.9.2` 的 `installDispatchImageAdmission` 成功满足了 DSH 核心层 `LlmService.generate` 的模态检查,将图片放行给底层适配器。但在以 `dsh-llm-pi-ai` 作为适配器时,其内部在 `streamWithSnapshot` 中执行了第二道门禁校验:
250
+
251
+ ```js
252
+ const model = this.modelOf(snapshot, options.provider, options.model);
253
+ if (containsImage && !model.input.includes("image"))
254
+ throw new LlmError(`pi-ai model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
255
+ ```
256
+
257
+ `this.modelOf` 直接读取适配器自身维护的 `snapshot.models` 目录,若用户未在 `settings.yaml` 中显式声明 `input: [text, image]`,此处默认只有 `["text"]`。导致未声明的模型在放行图片后,在流式发起前被 `pi-ai` 适配器抛出 `UNSUPPORTED_CONTENT`,且由于该图片已被写进会话历史,后续所有轮次(哪怕发纯文本)都会彻底报错锁死。
258
+
259
+ **修复**。在判定为直通路由(`direct: true`)时,`installDispatchImageAdmission` 增加了双重深度注入:
260
+ 1. 包装 `adapter.modelOf`(若存在),使其对直通路由返回携带 `"image"` 模态的模型对象;
261
+ 2. 动态向当前快照模型(`snapshot.models.getModel(route, model).input`)追加 `"image"` 模态;
262
+ 3. `dispose` 时完整还原 `adapter.modelOf` 并清理被追加的模态数组。
263
+
247
264
  ## v0.9.2:准入不等于派发
248
265
 
249
266
  **问题**。插件收集的每一个能力信号 —— `probeResults`、`multimodalModels`、
package/index.js CHANGED
@@ -427,15 +427,34 @@ function installDispatchImageAdmission(llm, getConfig, logger) {
427
427
  }
428
428
  if (llm[LLM_REGISTRATION_WRAP_MARK]) return () => {}; // already wrapped by us (HMR re-apply)
429
429
  const original = llm.registration.bind(llm);
430
- /** adapter -> the prepareCall it had before this wrap, for dispose. */
430
+ /** adapter -> { prepareCall, modelOf } original methods, for dispose. */
431
431
  const patched = new Map();
432
+ /** Set of arrays where we pushed "image", for cleanup on dispose. */
433
+ const modifiedInputs = new Set();
434
+ /** Keyed routes (probeKey) known to have direct dispatch permission. */
435
+ const directRouteCache = new Set();
436
+
432
437
  const wrapped = (provider) => {
433
438
  const registration = original(provider);
434
439
  const adapter = registration?.adapter;
435
440
  if (adapter !== undefined && adapter !== null
436
441
  && !patched.has(adapter) && typeof adapter.prepareCall === "function") {
437
442
  const rawPrepare = adapter.prepareCall;
443
+ const rawModelOf = typeof adapter.modelOf === "function" ? adapter.modelOf : undefined;
438
444
  const prepare = rawPrepare.bind(adapter);
445
+
446
+ if (rawModelOf !== undefined) {
447
+ adapter.modelOf = function (snapshot, route, model) {
448
+ const resolved = rawModelOf.call(this, snapshot, route, model);
449
+ if (resolved !== undefined && resolved !== null
450
+ && Array.isArray(resolved.input) && !resolved.input.includes("image")
451
+ && directRouteCache.has(probeKey(route, model))) {
452
+ return { ...resolved, input: [...resolved.input, "image"] };
453
+ }
454
+ return resolved;
455
+ };
456
+ }
457
+
439
458
  adapter.prepareCall = async (route, model, signal) => {
440
459
  const call = await prepare(route, model, signal);
441
460
  if (call === undefined || call === null || call.model === undefined || call.model === null) return call;
@@ -449,10 +468,31 @@ function installDispatchImageAdmission(llm, getConfig, logger) {
449
468
  // route may reject: leave the core's projection in place.
450
469
  return call;
451
470
  }
452
- if (!direct) return call;
471
+ if (!direct) {
472
+ directRouteCache.delete(probeKey(route, model));
473
+ return call;
474
+ }
475
+ directRouteCache.add(probeKey(route, model));
476
+
477
+ // In dsh-llm-pi-ai, adapter.streamWithSnapshot checks:
478
+ // if (containsImage && !model.input.includes("image")) throw LlmError(...)
479
+ // where model is obtained via this.modelOf(snapshot, provider, model) -> snapshot.models.getModel(route, model).
480
+ // Ensure that the adapter snapshot's model also admits images so the adapter-level guard passes.
481
+ try {
482
+ const snapshot = typeof adapter.current === "function" ? adapter.current() : undefined;
483
+ const targetModel = snapshot?.models?.getModel?.(route, model);
484
+ if (targetModel !== undefined && targetModel !== null
485
+ && Array.isArray(targetModel.input) && !targetModel.input.includes("image")) {
486
+ targetModel.input.push("image");
487
+ modifiedInputs.add(targetModel.input);
488
+ }
489
+ } catch {
490
+ /* best-effort */
491
+ }
492
+
453
493
  return { ...call, model: { ...call.model, inputModalities: [...(mods ?? []), "image"] } };
454
494
  };
455
- patched.set(adapter, rawPrepare);
495
+ patched.set(adapter, { prepareCall: rawPrepare, modelOf: rawModelOf });
456
496
  }
457
497
  return registration;
458
498
  };
@@ -460,10 +500,17 @@ function installDispatchImageAdmission(llm, getConfig, logger) {
460
500
  llm.registration = wrapped;
461
501
  logger?.debug?.("[tool-vision] dispatch image admission installed (llm.registration wrapped)");
462
502
  return () => {
463
- for (const [adapter, prepare] of patched) {
464
- if (typeof prepare === "function") adapter.prepareCall = prepare;
503
+ for (const [adapter, originalMethods] of patched) {
504
+ if (typeof originalMethods.prepareCall === "function") adapter.prepareCall = originalMethods.prepareCall;
505
+ if (typeof originalMethods.modelOf === "function") adapter.modelOf = originalMethods.modelOf;
465
506
  }
466
507
  patched.clear();
508
+ for (const arr of modifiedInputs) {
509
+ const idx = arr.indexOf("image");
510
+ if (idx !== -1) arr.splice(idx, 1);
511
+ }
512
+ modifiedInputs.clear();
513
+ directRouteCache.clear();
467
514
  if (llm.registration === wrapped) llm.registration = original;
468
515
  delete llm[LLM_REGISTRATION_WRAP_MARK];
469
516
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tool-vision",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "DeepSeek Harness 外置视觉模型插件:inspect_image 把本地图片或 http(s) 图片 URL 发给任意 OpenAI 兼容端点,视觉模型看图的文字回答直接带回对话;附 Web UI 设置栏。",
5
5
  "type": "module",
6
6
  "main": "index.js",