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 +25 -0
- package/README.zh.md +17 -0
- package/index.js +52 -5
- package/package.json +1 -1
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 ->
|
|
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)
|
|
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,
|
|
464
|
-
if (typeof
|
|
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