dsh-vision-router 1.4.2 → 1.4.4
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 +28 -6
- package/README.zh.md +28 -6
- package/index.js +170 -80
- package/lib/client.js +86 -58
- package/lib/vision-resilience.js +7 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.4.
|
|
19
|
-
<a href="tests"><img src="https://img.shields.io/badge/verified-
|
|
18
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.4.4"><img src="https://img.shields.io/badge/release-v1.4.4-5B4CF0?style=flat-square" alt="Release v1.4.4" /></a>
|
|
19
|
+
<a href="tests"><img src="https://img.shields.io/badge/verified-257%20tests-2EA44F?style=flat-square" alt="Verified: 257 tests" /></a>
|
|
20
20
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
21
21
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
22
22
|
<img src="https://img.shields.io/badge/runtime-no%20Python-8A2BE2?style=flat-square" alt="No Python" />
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **Announcement (v1.4.
|
|
31
|
+
> 📌 **Announcement (v1.4.4)**
|
|
32
32
|
>
|
|
33
|
-
> **v1.4.
|
|
33
|
+
> **v1.4.4:** Fixes the blank DSH rc.6 plugin settings page by declaring the client services required by settingsScope.
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<img src="assets/vision-demo.gif" width="640" alt="Demo: paste an image, the agent locates the send button with vision_ground / vision_crop / vision_pixel_diff and answers with coordinates" />
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
- [How it compares](#how-it-compares)
|
|
43
43
|
- [Acknowledgements](#acknowledgements)
|
|
44
44
|
- [Quick start](#quick-start)
|
|
45
|
+
- [Free vision key channels](#free-vision-key-channels)
|
|
45
46
|
- [Highlights](#highlights)
|
|
46
47
|
- [How it works](#how-it-works)
|
|
47
48
|
- [Tools](#tools)
|
|
@@ -143,7 +144,7 @@ The auto-vision group follows the live DSH model catalog. Adding models or chang
|
|
|
143
144
|
|
|
144
145
|
After choosing the “+ Auto Vision” group, paste or upload an image normally. By default the complete vision tool schema is stable from session start, so the agent can immediately use `vision_describe`, `vision_ground`, `vision_crop`, and the rest across multiple steps when needed.
|
|
145
146
|
|
|
146
|
-
The built-in anonymous OVH vision fallback is already configured, so normal image use needs no signup or API key. **The lower-right chat picker selects only the brain/conversation model**; vision backends do not belong there. Advanced options live under **Settings → Plugins → Plugin config → 视觉路由(自动识图)**: each vision-backend row
|
|
147
|
+
The built-in anonymous OVH vision fallback is already configured, so normal image use needs no signup or API key. **The lower-right chat picker selects only the brain/conversation model**; vision backends do not belong there. Advanced options live under **Settings → Plugins → Plugin config → 视觉路由(自动识图)**: each vision-backend row may select any callable generative user model already configured under **Settings → Models**. DSH image-capability metadata is advisory only: undeclared or text-only-labelled models remain selectable and show a warning. At runtime Vision Router always tries the provider's registered DSH adapter first — including WebSocket, RPC and private transports — and falls through on a real failure. The direct compatibility bridge is used only when an http(s) OpenAI Chat Completions endpoint is positively identified. Leaving every user row empty is valid; the OVH chain remains the final fallback. `Vision HTTP` is an internal transport route, not a model group users should select.
|
|
147
148
|
|
|
148
149
|
### See it in action
|
|
149
150
|
|
|
@@ -154,6 +155,27 @@ The built-in anonymous OVH vision fallback is already configured, so normal imag
|
|
|
154
155
|
<img src="assets/dsh-conversation-image-qa-result.png" width="49%" alt="The agent's structured answer describing the image content." />
|
|
155
156
|
</p>
|
|
156
157
|
|
|
158
|
+
## Free vision key channels
|
|
159
|
+
|
|
160
|
+
The built-in OVH fallback is anonymous by design, and OVH caps anonymous use at **2 requests/minute per IP per model**. If that feels tight, every channel below offers **free vision models with much higher quotas** — all of them are free to register, and none charges for the free tier. Free policies rotate often; treat this table as an August 2026 snapshot and double-check each provider's console before relying on it.
|
|
161
|
+
|
|
162
|
+
| Channel | Free vision model(s) | Free quota | CN direct? | Where to get the key |
|
|
163
|
+
|---|---|---|---|---|
|
|
164
|
+
| OVHcloud AI Endpoints (access key) | `Qwen2.5-VL-72B-Instruct` — the same endpoint the built-in fallback uses | **400 req/min** per project per model (vs 2 anonymous) | ✅ | OVH account → Public Cloud project (attach a payment method; free models are not charged) → AI Endpoints access key |
|
|
165
|
+
| Zhipu (bigmodel.cn) | `glm-4.6v-flash` · `glm-4.1v-thinking-flash` · `glm-4v-flash` — three permanently free models; chaining them triples capacity | uncapped tokens | ✅ | open.bigmodel.cn → API keys |
|
|
166
|
+
| DashScope (Aliyun) | `qwen3-vl-flash` (limited-time free) and the Qwen-VL series | new users: 1M tokens per model series / 90 days | ✅ | bailian.console.aliyun.com |
|
|
167
|
+
| Intern AI (Shanghai AI Lab) | `internvl-latest` · `internvl3.5-latest` | 30 RPM, **90M tokens/month** | ✅ | chat.intern-ai.org.cn |
|
|
168
|
+
| Groq | `meta-llama/llama-4-scout-17b-16e-instruct` (native multimodal, up to 5 images) | 30 RPM / 14,400 req/day, no card | ❌ proxy | console.groq.com |
|
|
169
|
+
| Google AI Studio | `gemini-2.5-flash` · `gemini-2.5-flash-lite` | 10–30 RPM / 500–1,500 req/day | ❌ proxy | aistudio.google.com |
|
|
170
|
+
| NVIDIA NIM | `meta/llama-3.2-11b-vision-instruct` · `nvidia/nemotron-nano-12b-v2-vl` | 40 RPM, no card | ⚠️ | build.nvidia.com |
|
|
171
|
+
| OpenCode Zen | `mimo-v2.5-free` (vision + code) | 30 RPM / 500 req/day | ⚠️ | opencode.ai/zen |
|
|
172
|
+
| OpenRouter | `google/gemma-4-26b-a4b-it:free` · `google/gemma-4-31b-it:free` | 50 req/day on unpaid accounts | ❌ proxy | openrouter.ai |
|
|
173
|
+
|
|
174
|
+
Any of these channels can join the vision chain as an `httpProviders` entry (key in the matching environment variable or `~/.dsh/.credentials.yaml`), and the chain tries your entries before the anonymous fallback.
|
|
175
|
+
|
|
176
|
+
> [!NOTE]
|
|
177
|
+
> Free-tier policies change without notice — Cerebras retired its free tier in July 2026 (now a one-time $5 credit), SambaNova's free tier is down to 20 requests/day, and Hugging Face's is $0.10/month. Third-party “`:free` relay” aggregators are deliberately not listed: they rotate quickly, lack SLAs, and some resell quota in ways that violate upstream terms.
|
|
178
|
+
|
|
157
179
|
## Highlights
|
|
158
180
|
|
|
159
181
|
- **Original pixels, real answers.** The vision chain reads the image at original resolution (auto-downscaled only to protect latency/quota); the agent's question travels with the image, so answers are about *your* question, not a generic description.
|
|
@@ -226,7 +248,7 @@ The vision tools try backends in order and surface an error only after all of th
|
|
|
226
248
|
|
|
227
249
|
1. **User vision models**: one per settings row, top to bottom; only models under **Settings → Models** that explicitly declare image input are shown;
|
|
228
250
|
2. **Advanced custom HTTP vision endpoints**: legacy/advanced `httpProviders`, when present, run after the user models;
|
|
229
|
-
3. **Built-in anonymous OVH fallback**: always last and never exposed in a model picker. The current quality-first chain is `Qwen3.5-397B-A17B` → `Qwen2.5-VL-72B-Instruct` → `Qwen3.6-27B` → `Mistral-Small-3.2-24B-Instruct-2506` → `Qwen3.5-9B`. OVH anonymous limits are **2 requests/minute per IP per model**. The five models have independent buckets, so spreading requests across them is about **10 RPM in theory**, subject to OVH's actual rate limiting. No signup or API key is required.
|
|
251
|
+
3. **Built-in anonymous OVH fallback**: always last and never exposed in a model picker. The current quality-first chain is `Qwen3.5-397B-A17B` → `Qwen2.5-VL-72B-Instruct` → `Qwen3.6-27B` → `Mistral-Small-3.2-24B-Instruct-2506` → `Qwen3.5-9B`. OVH anonymous limits are **2 requests/minute per IP per model**. The five models have independent buckets, so spreading requests across them is about **10 RPM in theory**, subject to OVH's actual rate limiting. No signup or API key is required. Want more headroom? See [Free vision key channels](#free-vision-key-channels) — a free OVH access key lifts this same endpoint to 400 requests/minute.
|
|
230
252
|
|
|
231
253
|
> [!IMPORTANT]
|
|
232
254
|
> This “vision chain” is the **eyes** used by Vision Router: each settings row selects one user vision model, while the lower-right chat picker selects the **brain/conversation model**. The two are deliberately separate. Text-only DeepSeek/opencode models are filtered out of the vision-backend dropdown, and the internal `Vision HTTP` transport route is no longer exposed to users.
|
package/README.zh.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.4.
|
|
19
|
-
<a href="tests"><img src="https://img.shields.io/badge/verified-
|
|
18
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.4.4"><img src="https://img.shields.io/badge/release-v1.4.4-5B4CF0?style=flat-square" alt="Release v1.4.4" /></a>
|
|
19
|
+
<a href="tests"><img src="https://img.shields.io/badge/verified-257%20tests-2EA44F?style=flat-square" alt="Verified: 257 tests" /></a>
|
|
20
20
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
21
21
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
22
22
|
<img src="https://img.shields.io/badge/runtime-no%20Python-8A2BE2?style=flat-square" alt="No Python" />
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **公告(v1.4.
|
|
31
|
+
> 📌 **公告(v1.4.4)**
|
|
32
32
|
>
|
|
33
|
-
> **v1.4.
|
|
33
|
+
> **v1.4.4:修复 DSH rc.6 插件配置页空白。**
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
- [对比同类插件](#对比同类插件)
|
|
43
43
|
- [致谢](#致谢)
|
|
44
44
|
- [快速开始](#快速开始)
|
|
45
|
+
- [免费视觉 Key 渠道](#免费视觉-key-渠道)
|
|
45
46
|
- [亮点](#亮点)
|
|
46
47
|
- [工作原理](#工作原理)
|
|
47
48
|
- [工具](#工具)
|
|
@@ -141,7 +142,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
141
142
|
|
|
142
143
|
选好「+ 自动识图」模型组后,直接往对话里贴图即可。默认情况下完整视觉工具表从会话开始就保持稳定,Agent 可直接调用 `vision_describe`、`vision_ground`、`vision_crop` 等工具看图,需要时连续多步操作。
|
|
143
144
|
|
|
144
|
-
默认已经有内置 OVH 匿名视觉兜底,无需注册、无需 Key。**聊天页右下角只选择“脑子/会话模型”**;视觉模型不要在那里选。高级配置在 **设置 → 插件 → 插件配置 →
|
|
145
|
+
默认已经有内置 OVH 匿名视觉兜底,无需注册、无需 Key。**聊天页右下角只选择“脑子/会话模型”**;视觉模型不要在那里选。高级配置在 **设置 → 插件 → 插件配置 → 视觉路由(自动识图)**:视觉后端链每一行都可以选择 **设置 → 模型** 中任意可调用的生成式用户模型。DSH 的图片能力声明现在只作提示:未声明图片能力、甚至被标成仅文本的模型也会列出并给出警告。运行时永远先通过该供应商已注册的 DSH adapter 实际调用,因此 WebSocket、RPC 和私有协议都保留原生传输;只有明确识别为 http(s) OpenAI Chat Completions 的渠道才可能进入 HTTP 直连兼容兜底。实际调用失败后自动尝试下一后端;一行都不填也可以,OVH 免费链会固定在最后兜底。插件内部的 `Vision HTTP` 只是传输实现,不是用户需要选择的模型组。
|
|
145
146
|
|
|
146
147
|
### 实际效果
|
|
147
148
|
|
|
@@ -152,6 +153,27 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
152
153
|
<img src="assets/dsh-conversation-image-qa-result.png" width="49%" alt="Agent 对图片内容的结构化解读。" />
|
|
153
154
|
</p>
|
|
154
155
|
|
|
156
|
+
## 免费视觉 Key 渠道
|
|
157
|
+
|
|
158
|
+
内置 OVH 兜底是匿名设计,OVH 对匿名访问的限制是**每 IP、每模型 2 次/分钟**。觉得不够用时,下面这些渠道都有**免费且额度大得多的视觉模型**——全部免费注册,无需为免费档付费。免费政策轮换频繁,下表是 2026 年 8 月快照,依赖前请以各家控制台为准。
|
|
159
|
+
|
|
160
|
+
| 渠道 | 免费视觉模型 | 免费额度 | 大陆直连 | Key 领取 |
|
|
161
|
+
|---|---|---|---|---|
|
|
162
|
+
| OVHcloud AI Endpoints(access key) | `Qwen2.5-VL-72B-Instruct`——与内置兜底同一个端点 | **400 次/分钟**/项目/模型(对比匿名 2 次/分钟) | ✅ | 注册 OVH 账号 → Public Cloud 项目(需挂支付方式;免费模型不扣费)→ AI Endpoints access key |
|
|
163
|
+
| 智谱(bigmodel.cn) | `glm-4.6v-flash` · `glm-4.1v-thinking-flash` · `glm-4v-flash`——三个永久免费模型,串起来容量 ×3 | token 不限量 | ✅ | open.bigmodel.cn → API keys |
|
|
164
|
+
| 阿里云百炼 | `qwen3-vl-flash`(限免)与 Qwen-VL 系列 | 新用户每模型系列 100 万 token / 90 天 | ✅ | bailian.console.aliyun.com |
|
|
165
|
+
| Intern AI(上海AI实验室) | `internvl-latest` · `internvl3.5-latest` | 30 RPM,**9000 万 token/月** | ✅ | chat.intern-ai.org.cn |
|
|
166
|
+
| Groq | `meta-llama/llama-4-scout-17b-16e-instruct`(原生多模态,最多 5 张图) | 30 RPM / 14,400 次/天,免卡 | ❌ 需代理 | console.groq.com |
|
|
167
|
+
| Google AI Studio | `gemini-2.5-flash` · `gemini-2.5-flash-lite` | 10–30 RPM / 500–1,500 次/天 | ❌ 需代理 | aistudio.google.com |
|
|
168
|
+
| NVIDIA NIM | `meta/llama-3.2-11b-vision-instruct` · `nvidia/nemotron-nano-12b-v2-vl` | 40 RPM,免卡 | ⚠️ | build.nvidia.com |
|
|
169
|
+
| OpenCode Zen | `mimo-v2.5-free`(视觉 + 代码) | 30 RPM / 500 次/天 | ⚠️ | opencode.ai/zen |
|
|
170
|
+
| OpenRouter | `google/gemma-4-26b-a4b-it:free` · `google/gemma-4-31b-it:free` | 未充值账户 50 次/天 | ❌ 需代理 | openrouter.ai |
|
|
171
|
+
|
|
172
|
+
以上渠道都能以 `httpProviders` 条目加入视觉链(Key 放对应环境变量或 `~/.dsh/.credentials.yaml`),链路会先尝试你的条目、再落到匿名兜底。
|
|
173
|
+
|
|
174
|
+
> [!NOTE]
|
|
175
|
+
> 免费政策随时可能调整——Cerebras 已在 2026 年 7 月取消免费档(改为一次性 $5 赠金),SambaNova 免费档收紧到 20 次/天,Hugging Face 只剩 $0.10/月。第三方“`:free` 中转”聚合站刻意不列入:轮换频繁、无 SLA,部分还存在违反上游条款的转售行为。
|
|
176
|
+
|
|
155
177
|
## 亮点
|
|
156
178
|
|
|
157
179
|
- **原图像素,真实答案。** 视觉链按原始分辨率读图(仅为保护延迟/额度自动缩放);你的问题随图一起发送,答案围绕*你的问题*,而不是一段泛泛的描述。
|
|
@@ -224,7 +246,7 @@ vision_long_screenshot_ocr image="chat-log.png" chunkHeight=1200 overlap=120
|
|
|
224
246
|
|
|
225
247
|
1. **用户视觉模型**:设置卡里一行一个,从上到下;只显示 **设置 → 模型** 中明确声明支持 image 输入的模型;
|
|
226
248
|
2. **高级自定义 HTTP 视觉端点**:如果旧配置/高级配置中存在 `httpProviders`,在用户模型之后尝试;
|
|
227
|
-
3. **内置 OVH 匿名免费兜底**:固定最后尝试,不需要出现在任何模型选择器里。当前内置链按质量优先为 `Qwen3.5-397B-A17B` → `Qwen2.5-VL-72B-Instruct` → `Qwen3.6-27B` → `Mistral-Small-3.2-24B-Instruct-2506` → `Qwen3.5-9B`。OVH 匿名限额为 **每 IP、每模型 2 次/分钟**;5 个模型是独立限额,因此理论上分散请求可到约 **10 次/分钟**,实际仍以 OVH 当时的限流为准。免注册、免 Key
|
|
249
|
+
3. **内置 OVH 匿名免费兜底**:固定最后尝试,不需要出现在任何模型选择器里。当前内置链按质量优先为 `Qwen3.5-397B-A17B` → `Qwen2.5-VL-72B-Instruct` → `Qwen3.6-27B` → `Mistral-Small-3.2-24B-Instruct-2506` → `Qwen3.5-9B`。OVH 匿名限额为 **每 IP、每模型 2 次/分钟**;5 个模型是独立限额,因此理论上分散请求可到约 **10 次/分钟**,实际仍以 OVH 当时的限流为准。免注册、免 Key。想提额度?详见[免费视觉 Key 渠道](#免费视觉-key-渠道)——同一个端点挂免费 access key 后是 400 次/分钟。
|
|
228
250
|
|
|
229
251
|
> [!IMPORTANT]
|
|
230
252
|
> 这里的“视觉链”是 Vision Router 调用的**眼睛**:设置页里每一行只选一个用户视觉模型;聊天页右下角选择的是**脑子/会话模型**,两者完全分开。纯文本 DeepSeek / opencode 不会出现在视觉后端下拉里;内部 `Vision HTTP` 也不会再暴露给用户。
|
package/index.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
createVisionCircuitBreaker,
|
|
46
46
|
createVisionTurnMemory,
|
|
47
47
|
buildVisionFailure,
|
|
48
|
+
ensureSentencePunctuation,
|
|
48
49
|
resultCodeForKinds,
|
|
49
50
|
qwenKeyEndpointHint,
|
|
50
51
|
kindForHttpStatus,
|
|
@@ -2171,32 +2172,51 @@ export function decideVisionBackendCapability(info, provider, model, extraVision
|
|
|
2171
2172
|
const forced =
|
|
2172
2173
|
modelId !== '' &&
|
|
2173
2174
|
extras.some((entry) => entry === modelId || (providerId !== '' && entry === `${providerId}/${modelId}`))
|
|
2174
|
-
|
|
2175
|
-
//
|
|
2175
|
+
|
|
2176
|
+
// Capability metadata is ADVISORY. A user-selected generative model is
|
|
2177
|
+
// allowed to prove itself by an actual adapter call even when DSH omitted
|
|
2178
|
+
// image metadata or explicitly reports text-only input. The only hard gate
|
|
2179
|
+
// here is structural: endpoints that cannot produce an assistant answer
|
|
2180
|
+
// (embedding/reranker) are never valid vision backends.
|
|
2176
2181
|
if (forced) {
|
|
2177
|
-
return {
|
|
2182
|
+
return {
|
|
2183
|
+
image: true,
|
|
2184
|
+
attemptable: true,
|
|
2185
|
+
inputModalities: [...new Set([...inputModalities, 'image'])],
|
|
2186
|
+
inferred: 'override',
|
|
2187
|
+
reason: undefined,
|
|
2188
|
+
}
|
|
2178
2189
|
}
|
|
2179
|
-
// A model can consume images and still be the wrong KIND of endpoint
|
|
2180
|
-
// for this plugin: embedding/reranking produces no assistant answer.
|
|
2181
2190
|
if (modelId !== '' && looksLikeNonGenerativeVisionModel(modelId)) {
|
|
2182
2191
|
return {
|
|
2183
2192
|
image: false,
|
|
2193
|
+
attemptable: false,
|
|
2184
2194
|
inputModalities,
|
|
2185
2195
|
inferred: false,
|
|
2186
2196
|
reason: 'model name indicates an embedding/reranker endpoint, not a generative vision backend',
|
|
2187
2197
|
}
|
|
2188
2198
|
}
|
|
2189
2199
|
if (inputModalities.includes('image')) {
|
|
2190
|
-
return { image: true, inputModalities, inferred: false, reason: undefined }
|
|
2200
|
+
return { image: true, attemptable: true, inputModalities, inferred: false, reason: undefined }
|
|
2191
2201
|
}
|
|
2192
2202
|
if (modelId !== '' && looksLikeVisionModel(modelId)) {
|
|
2193
|
-
return {
|
|
2203
|
+
return {
|
|
2204
|
+
image: true,
|
|
2205
|
+
attemptable: true,
|
|
2206
|
+
inputModalities: [...new Set([...inputModalities, 'image'])],
|
|
2207
|
+
inferred: 'name',
|
|
2208
|
+
reason: undefined,
|
|
2209
|
+
}
|
|
2194
2210
|
}
|
|
2195
2211
|
return {
|
|
2196
2212
|
image: false,
|
|
2213
|
+
attemptable: true,
|
|
2197
2214
|
inputModalities,
|
|
2198
2215
|
inferred: false,
|
|
2199
|
-
reason:
|
|
2216
|
+
reason:
|
|
2217
|
+
inputModalities.length > 0
|
|
2218
|
+
? 'model metadata declares no image input'
|
|
2219
|
+
: 'model metadata does not declare image input',
|
|
2200
2220
|
}
|
|
2201
2221
|
}
|
|
2202
2222
|
|
|
@@ -2239,6 +2259,19 @@ export function resolveChannelBridgeTransport(rawProfile, resolvedProfile, model
|
|
|
2239
2259
|
}
|
|
2240
2260
|
}
|
|
2241
2261
|
|
|
2262
|
+
/** True only for a transport we can safely send through fetch + Chat Completions. */
|
|
2263
|
+
export function isOpenAIHttpBridgeTransport(transport) {
|
|
2264
|
+
if (!transport || transport.api !== 'openai-completions' || typeof transport.baseURL !== 'string') {
|
|
2265
|
+
return false
|
|
2266
|
+
}
|
|
2267
|
+
try {
|
|
2268
|
+
const url = new URL(transport.baseURL)
|
|
2269
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
2270
|
+
} catch {
|
|
2271
|
+
return false
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2242
2275
|
export function apply(ctx, config = {}) {
|
|
2243
2276
|
// Route sharp version diagnostics (issue #75) through the harness logger
|
|
2244
2277
|
// instead of console.warn, so the warning lands in the server log.
|
|
@@ -3005,28 +3038,37 @@ export function apply(ctx, config = {}) {
|
|
|
3005
3038
|
|
|
3006
3039
|
const resolveVisionBackendCapability = async (provider, model) => {
|
|
3007
3040
|
if (typeof provider !== 'string' || provider === '' || typeof model !== 'string' || model === '') {
|
|
3008
|
-
return { image: false, inputModalities: [], reason: 'missing provider/model' }
|
|
3041
|
+
return { image: false, attemptable: false, inputModalities: [], reason: 'missing provider/model' }
|
|
3009
3042
|
}
|
|
3010
3043
|
if (provider !== HTTP_ROUTE && isGeneratedVisionWrapperRoute(provider)) {
|
|
3011
|
-
return {
|
|
3044
|
+
return {
|
|
3045
|
+
image: false,
|
|
3046
|
+
attemptable: false,
|
|
3047
|
+
inputModalities: [],
|
|
3048
|
+
reason: 'generated auto-vision wrapper, not a vision backend',
|
|
3049
|
+
}
|
|
3012
3050
|
}
|
|
3013
3051
|
if (!adapterAvailable(ctx.llm, provider)) {
|
|
3014
|
-
return {
|
|
3052
|
+
return {
|
|
3053
|
+
image: false,
|
|
3054
|
+
attemptable: false,
|
|
3055
|
+
inputModalities: [],
|
|
3056
|
+
reason: 'provider adapter is not registered',
|
|
3057
|
+
}
|
|
3015
3058
|
}
|
|
3016
3059
|
try {
|
|
3017
3060
|
const info = await ctx.llm.resolveModelInfo(provider, model)
|
|
3018
3061
|
return decideVisionBackendCapability(info, provider, model, current().extraVisionModels)
|
|
3019
3062
|
} catch (error) {
|
|
3020
|
-
//
|
|
3021
|
-
//
|
|
3022
|
-
//
|
|
3063
|
+
// Custom/WebSocket/private adapters can be perfectly callable while
|
|
3064
|
+
// their model metadata is incomplete or not resolvable. Preserve the
|
|
3065
|
+
// structural decision and surface the lookup failure only as advisory
|
|
3066
|
+
// diagnostics; the real adapter call is the source of truth.
|
|
3023
3067
|
const fallback = decideVisionBackendCapability(undefined, provider, model, current().extraVisionModels)
|
|
3024
|
-
if (fallback.image)
|
|
3025
|
-
|
|
3026
|
-
image: false,
|
|
3027
|
-
inputModalities: [],
|
|
3028
|
-
reason: error && error.message ? error.message : String(error),
|
|
3068
|
+
if (!fallback.image) {
|
|
3069
|
+
fallback.reason = `capability metadata unavailable: ${error && error.message ? error.message : String(error)}`
|
|
3029
3070
|
}
|
|
3071
|
+
return fallback
|
|
3030
3072
|
}
|
|
3031
3073
|
}
|
|
3032
3074
|
|
|
@@ -3096,12 +3138,16 @@ export function apply(ctx, config = {}) {
|
|
|
3096
3138
|
if (!transport.baseURL) {
|
|
3097
3139
|
return { ok: false, reason: 'no resolved channel baseURL', rawProfile, resolvedProfile, transport }
|
|
3098
3140
|
}
|
|
3099
|
-
//
|
|
3100
|
-
//
|
|
3101
|
-
|
|
3141
|
+
// This compatibility bridge is deliberately transport-specific. The
|
|
3142
|
+
// normal path always delegates to DSH's registered adapter, which may be
|
|
3143
|
+
// HTTP, WebSocket, RPC or a private protocol. Only a positively identified
|
|
3144
|
+
// http(s) OpenAI Chat Completions endpoint may bypass it.
|
|
3145
|
+
if (!isOpenAIHttpBridgeTransport(transport)) {
|
|
3102
3146
|
return {
|
|
3103
3147
|
ok: false,
|
|
3104
|
-
reason:
|
|
3148
|
+
reason:
|
|
3149
|
+
`channel transport ${transport.api || 'unknown'} @ ${transport.baseURL || 'unknown'} ` +
|
|
3150
|
+
'is not an http(s) OpenAI Chat Completions endpoint',
|
|
3105
3151
|
rawProfile,
|
|
3106
3152
|
resolvedProfile,
|
|
3107
3153
|
transport,
|
|
@@ -3261,6 +3307,56 @@ export function apply(ctx, config = {}) {
|
|
|
3261
3307
|
})
|
|
3262
3308
|
}
|
|
3263
3309
|
|
|
3310
|
+
const configuredVisionPairKeys = () =>
|
|
3311
|
+
new Set(
|
|
3312
|
+
pairs()
|
|
3313
|
+
.filter((pair) => pair && pair.provider !== HTTP_ROUTE)
|
|
3314
|
+
.map((pair) => `${pair.provider}/${pair.model}`),
|
|
3315
|
+
)
|
|
3316
|
+
|
|
3317
|
+
const mayUseDirectChannelBridge = (pair, capability, classification) => {
|
|
3318
|
+
if (!pair || !classification) return false
|
|
3319
|
+
const kind = classification.kind
|
|
3320
|
+
if (
|
|
3321
|
+
kind !== VISION_FAILURE_KINDS.INVALID_REQUEST &&
|
|
3322
|
+
kind !== VISION_FAILURE_KINDS.NETWORK &&
|
|
3323
|
+
kind !== VISION_FAILURE_KINDS.OTHER
|
|
3324
|
+
) return false
|
|
3325
|
+
// Explicit selection is permission to TRY an undeclared model. Inferred /
|
|
3326
|
+
// manual-override backends keep the legacy bridge behavior. Transport is
|
|
3327
|
+
// still fail-closed: WebSocket/private adapters never get converted to
|
|
3328
|
+
// HTTP because channelBridgePlan() must positively identify http(s) +
|
|
3329
|
+
// OpenAI Chat Completions before a direct request is made.
|
|
3330
|
+
if (!configuredVisionPairKeys().has(`${pair.provider}/${pair.model}`) && !(capability && capability.inferred)) {
|
|
3331
|
+
return false
|
|
3332
|
+
}
|
|
3333
|
+
return channelBridgePlan(pair.provider, pair.model).ok === true
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
const callVisionPairWithOptionalBridge = async (pair, messages, options = {}) => {
|
|
3337
|
+
try {
|
|
3338
|
+
return await callVisionPair(pair, messages, options)
|
|
3339
|
+
} catch (error) {
|
|
3340
|
+
const classification = classifyVisionFailure(error)
|
|
3341
|
+
const capability =
|
|
3342
|
+
options.capability ?? (await resolveVisionBackendCapability(pair.provider, pair.model))
|
|
3343
|
+
if (
|
|
3344
|
+
mayUseDirectChannelBridge(pair, capability, classification) &&
|
|
3345
|
+
Array.isArray(options.bridgeBlocks) &&
|
|
3346
|
+
typeof options.bridgeInstruction === 'string'
|
|
3347
|
+
) {
|
|
3348
|
+
return directChannelVisionAnswer(
|
|
3349
|
+
pair.provider,
|
|
3350
|
+
pair.model,
|
|
3351
|
+
options.bridgeBlocks,
|
|
3352
|
+
options.bridgeInstruction,
|
|
3353
|
+
options.signal,
|
|
3354
|
+
)
|
|
3355
|
+
}
|
|
3356
|
+
throw error
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3264
3360
|
const collectVisionBackendCapabilities = async () => {
|
|
3265
3361
|
const capabilities = {}
|
|
3266
3362
|
if (typeof ctx.llm.listProviders !== 'function') return capabilities
|
|
@@ -3273,7 +3369,6 @@ export function apply(ctx, config = {}) {
|
|
|
3273
3369
|
for (const entry of providers) {
|
|
3274
3370
|
const provider = entry && typeof entry.id === 'string' ? entry.id : ''
|
|
3275
3371
|
if (provider === '') continue
|
|
3276
|
-
if (provider !== HTTP_ROUTE && isGeneratedVisionWrapperRoute(provider)) continue
|
|
3277
3372
|
let listed = []
|
|
3278
3373
|
try {
|
|
3279
3374
|
const registration = ctx.llm.registration(provider)
|
|
@@ -3294,11 +3389,11 @@ export function apply(ctx, config = {}) {
|
|
|
3294
3389
|
}
|
|
3295
3390
|
|
|
3296
3391
|
|
|
3297
|
-
// Build the tool-side adapter chain
|
|
3298
|
-
//
|
|
3299
|
-
//
|
|
3300
|
-
//
|
|
3301
|
-
//
|
|
3392
|
+
// Build the tool-side adapter chain. Explicit rows are user intent: every
|
|
3393
|
+
// structurally callable generative backend gets a real adapter attempt even
|
|
3394
|
+
// when DSH does not declare image input. Auto-discovery remains conservative
|
|
3395
|
+
// and only appends models positively identified as visual. This avoids
|
|
3396
|
+
// silently trying every text model while making custom providers reliable.
|
|
3302
3397
|
const resolveToolVisionPairs = async () => {
|
|
3303
3398
|
const out = []
|
|
3304
3399
|
const seen = new Set()
|
|
@@ -3313,14 +3408,14 @@ export function apply(ctx, config = {}) {
|
|
|
3313
3408
|
if (!pair || pair.provider === HTTP_ROUTE) continue
|
|
3314
3409
|
if (!adapterAvailable(ctx.llm, pair.provider)) continue
|
|
3315
3410
|
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
3316
|
-
if (capability.
|
|
3411
|
+
if (capability.attemptable !== false) add(pair.provider, pair.model)
|
|
3317
3412
|
}
|
|
3318
3413
|
|
|
3319
3414
|
const capabilities = await collectVisionBackendCapabilities()
|
|
3320
3415
|
for (const [provider, models] of Object.entries(capabilities)) {
|
|
3321
3416
|
if (provider === HTTP_ROUTE || ownRoutes().has(provider)) continue
|
|
3322
3417
|
for (const [model, capability] of Object.entries(models ?? {})) {
|
|
3323
|
-
if (capability && capability.image) add(provider, model)
|
|
3418
|
+
if (capability && capability.attemptable !== false && capability.image) add(provider, model)
|
|
3324
3419
|
}
|
|
3325
3420
|
}
|
|
3326
3421
|
return out
|
|
@@ -3349,8 +3444,9 @@ export function apply(ctx, config = {}) {
|
|
|
3349
3444
|
async listModels() {
|
|
3350
3445
|
const entries = []
|
|
3351
3446
|
for (const pair of pairs()) {
|
|
3447
|
+
if (!adapterAvailable(ctx.llm, pair.provider)) continue
|
|
3352
3448
|
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
3353
|
-
if (
|
|
3449
|
+
if (capability.attemptable === false) continue
|
|
3354
3450
|
entries.push({
|
|
3355
3451
|
provider: chainRoute(),
|
|
3356
3452
|
id: `${pair.provider}/${pair.model}`,
|
|
@@ -3425,18 +3521,26 @@ export function apply(ctx, config = {}) {
|
|
|
3425
3521
|
continue
|
|
3426
3522
|
}
|
|
3427
3523
|
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
3428
|
-
if (
|
|
3524
|
+
if (capability.attemptable === false) {
|
|
3429
3525
|
failures.push(
|
|
3430
|
-
`${pair.provider}/${pair.model}:
|
|
3526
|
+
`${pair.provider}/${pair.model}: structurally unavailable (${capability.reason ?? 'unknown reason'})`,
|
|
3431
3527
|
)
|
|
3432
3528
|
ctx.logger?.warn(
|
|
3433
|
-
'vision-router: chain skips %s/%s (
|
|
3529
|
+
'vision-router: chain skips %s/%s (structurally unavailable: %s)',
|
|
3434
3530
|
pair.provider,
|
|
3435
3531
|
pair.model,
|
|
3436
|
-
capability.reason ?? 'unknown
|
|
3532
|
+
capability.reason ?? 'unknown reason',
|
|
3437
3533
|
)
|
|
3438
3534
|
continue
|
|
3439
3535
|
}
|
|
3536
|
+
if (!capability.image) {
|
|
3537
|
+
ctx.logger?.info(
|
|
3538
|
+
'vision-router: chain tries %s/%s despite advisory image capability (%s)',
|
|
3539
|
+
pair.provider,
|
|
3540
|
+
pair.model,
|
|
3541
|
+
capability.reason ?? 'not declared',
|
|
3542
|
+
)
|
|
3543
|
+
}
|
|
3440
3544
|
let budget = defaultBudget
|
|
3441
3545
|
try {
|
|
3442
3546
|
const info = await ctx.llm.resolveModelInfo(pair.provider, pair.model)
|
|
@@ -4215,9 +4319,9 @@ export function apply(ctx, config = {}) {
|
|
|
4215
4319
|
continue
|
|
4216
4320
|
}
|
|
4217
4321
|
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
4218
|
-
if (
|
|
4322
|
+
if (capability.attemptable === false) {
|
|
4219
4323
|
rejectedPairs.push(
|
|
4220
|
-
`${pair.provider}/${pair.model}:
|
|
4324
|
+
`${pair.provider}/${pair.model}: structurally unavailable (${capability.reason ?? 'unknown reason'})`,
|
|
4221
4325
|
)
|
|
4222
4326
|
}
|
|
4223
4327
|
}
|
|
@@ -4277,7 +4381,14 @@ export function apply(ctx, config = {}) {
|
|
|
4277
4381
|
try {
|
|
4278
4382
|
let messages = baseMessages
|
|
4279
4383
|
const signal = combineSignals(deadline.signal(), AbortSignal.timeout(timeoutMs()))
|
|
4280
|
-
|
|
4384
|
+
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
4385
|
+
let text = await callVisionPairWithOptionalBridge(pair, messages, {
|
|
4386
|
+
maxTokens: 4096,
|
|
4387
|
+
signal,
|
|
4388
|
+
capability,
|
|
4389
|
+
bridgeBlocks: blocks,
|
|
4390
|
+
bridgeInstruction: promptText,
|
|
4391
|
+
})
|
|
4281
4392
|
if (wantJson) {
|
|
4282
4393
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4283
4394
|
const parsed = extractJson(text)
|
|
@@ -4300,7 +4411,14 @@ export function apply(ctx, config = {}) {
|
|
|
4300
4411
|
source: { kind: 'plugin', plugin: 'dsh-vision-router' },
|
|
4301
4412
|
},
|
|
4302
4413
|
]
|
|
4303
|
-
text = await
|
|
4414
|
+
text = await callVisionPairWithOptionalBridge(pair, messages, {
|
|
4415
|
+
maxTokens: 4096,
|
|
4416
|
+
signal,
|
|
4417
|
+
capability,
|
|
4418
|
+
bridgeBlocks: blocks,
|
|
4419
|
+
bridgeInstruction:
|
|
4420
|
+
promptText + '\n\nThat output was not valid JSON. Respond with ONLY a valid JSON object now.',
|
|
4421
|
+
})
|
|
4304
4422
|
}
|
|
4305
4423
|
}
|
|
4306
4424
|
const fallback = `vision_describe: the model did not produce valid JSON. Raw output:\n${text.slice(0, 2000)}`
|
|
@@ -4429,7 +4547,9 @@ export function apply(ctx, config = {}) {
|
|
|
4429
4547
|
// must see a retryable=false result code, not an "occasional glitch".
|
|
4430
4548
|
const reason =
|
|
4431
4549
|
errors.length > 0
|
|
4432
|
-
?
|
|
4550
|
+
? ensureSentencePunctuation(
|
|
4551
|
+
`All vision models failed: ${errors.slice(0, 6).join(' | ')}${errors.length > 6 ? ` | … ${errors.length - 6} more` : ''}`,
|
|
4552
|
+
)
|
|
4433
4553
|
: 'No vision-capable backend is configured.'
|
|
4434
4554
|
const failure = await visionFailureResult(
|
|
4435
4555
|
scope,
|
|
@@ -4595,9 +4715,9 @@ export function apply(ctx, config = {}) {
|
|
|
4595
4715
|
}
|
|
4596
4716
|
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
4597
4717
|
pairCapabilities.set(`${pair.provider}/${pair.model}`, capability)
|
|
4598
|
-
if (
|
|
4718
|
+
if (capability.attemptable === false) {
|
|
4599
4719
|
errors.push(
|
|
4600
|
-
`${pair.provider}/${pair.model}:
|
|
4720
|
+
`${pair.provider}/${pair.model}: structurally unavailable (${capability.reason ?? 'unknown reason'})`,
|
|
4601
4721
|
)
|
|
4602
4722
|
}
|
|
4603
4723
|
}
|
|
@@ -4628,53 +4748,21 @@ export function apply(ctx, config = {}) {
|
|
|
4628
4748
|
pairCapabilities.set(pairKey, pairCapability)
|
|
4629
4749
|
}
|
|
4630
4750
|
try {
|
|
4631
|
-
const text = await
|
|
4751
|
+
const text = await callVisionPairWithOptionalBridge(
|
|
4632
4752
|
pair,
|
|
4633
4753
|
[{ role: 'user', content: [block, { type: 'text', text: instruction }] }],
|
|
4634
4754
|
{
|
|
4635
4755
|
maxTokens: 4096,
|
|
4636
4756
|
signal: combineSignals(deadline.signal(), AbortSignal.timeout(timeoutMs())),
|
|
4757
|
+
capability: pairCapability,
|
|
4758
|
+
bridgeBlocks: [block],
|
|
4759
|
+
bridgeInstruction: instruction,
|
|
4637
4760
|
},
|
|
4638
4761
|
)
|
|
4639
4762
|
if (text && text.trim() !== '') return { ok: true, text: text.trim() }
|
|
4640
4763
|
} catch (error) {
|
|
4641
4764
|
const classification = classifyVisionFailure(error)
|
|
4642
4765
|
visionBreaker.record(pairKey, fingerprint, classification, scope)
|
|
4643
|
-
// Channels whose catalog does not declare image input reject images
|
|
4644
|
-
// at the adapter wire. When the backend was recognized by the name
|
|
4645
|
-
// inference or the extraVisionModels override, call the channel's
|
|
4646
|
-
// OpenAI-compatible endpoint directly with its own baseURL and
|
|
4647
|
-
// credential before giving up on this pair. Deterministic failures
|
|
4648
|
-
// (AUTH / RATE_LIMIT / TIMEOUT) are NOT bridged: the bridge uses the
|
|
4649
|
-
// same endpoint and credential, so it would fail identically.
|
|
4650
|
-
const capability = pairCapabilities.get(pairKey)
|
|
4651
|
-
const bridged =
|
|
4652
|
-
capability &&
|
|
4653
|
-
capability.inferred &&
|
|
4654
|
-
(classification.kind === VISION_FAILURE_KINDS.INVALID_REQUEST ||
|
|
4655
|
-
classification.kind === VISION_FAILURE_KINDS.NETWORK ||
|
|
4656
|
-
classification.kind === VISION_FAILURE_KINDS.OTHER)
|
|
4657
|
-
if (bridged) {
|
|
4658
|
-
try {
|
|
4659
|
-
const direct = await directChannelVisionAnswer(
|
|
4660
|
-
pair.provider,
|
|
4661
|
-
pair.model,
|
|
4662
|
-
[block],
|
|
4663
|
-
instruction,
|
|
4664
|
-
combineSignals(deadline.signal(), AbortSignal.timeout(timeoutMs())),
|
|
4665
|
-
)
|
|
4666
|
-
if (direct && direct.trim() !== '') return { ok: true, text: direct.trim() }
|
|
4667
|
-
} catch (bridgeError) {
|
|
4668
|
-
const bridgeClassification = classifyVisionFailure(bridgeError)
|
|
4669
|
-
visionBreaker.record(pairKey, fingerprint, bridgeClassification, scope)
|
|
4670
|
-
recordFailure(
|
|
4671
|
-
pairKey,
|
|
4672
|
-
bridgeClassification,
|
|
4673
|
-
`direct channel fallback failed (${bridgeError && bridgeError.message ? bridgeError.message : String(bridgeError)})`,
|
|
4674
|
-
)
|
|
4675
|
-
continue
|
|
4676
|
-
}
|
|
4677
|
-
}
|
|
4678
4766
|
recordFailure(pairKey, classification, error && error.message ? error.message : String(error))
|
|
4679
4767
|
}
|
|
4680
4768
|
}
|
|
@@ -5640,8 +5728,10 @@ export function apply(ctx, config = {}) {
|
|
|
5640
5728
|
const started = Date.now()
|
|
5641
5729
|
let first
|
|
5642
5730
|
for (const pair of pairs()) {
|
|
5731
|
+
if (!pair) continue
|
|
5732
|
+
if (pair.provider !== HTTP_ROUTE && !adapterAvailable(ctx.llm, pair.provider)) continue
|
|
5643
5733
|
const capability = await resolveVisionBackendCapability(pair.provider, pair.model)
|
|
5644
|
-
if (capability.
|
|
5734
|
+
if (capability.attemptable !== false) {
|
|
5645
5735
|
first = pair
|
|
5646
5736
|
break
|
|
5647
5737
|
}
|
package/lib/client.js
CHANGED
|
@@ -40,7 +40,7 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
guidePromptNavBody: '在设置面板左侧的导航里,点击被高亮的「插件」入口(或点「下一步」自动进入)。进入插件页后,我会自动展开 Vision Router 并定位到「视觉后端链」。',
|
|
41
41
|
guidePromptCancel: '结束引导',
|
|
42
42
|
guideChainTitle: '第 3 步 · 这里就是视觉模型',
|
|
43
|
-
guideChainBody: '
|
|
43
|
+
guideChainBody: '上面的每一行都是你选择的视觉后端候选,从上到下依次实际尝试;可以全部留空。DSH 的图片能力声明现在只作提示,不再决定能不能选。内置 OVH 免费链固定在最后自动兜底。选好后点击页面底部「保存」。',
|
|
44
44
|
guideDone: '完成引导',
|
|
45
45
|
pending: '未保存',
|
|
46
46
|
readOnly: '当前设置提供方只读。',
|
|
@@ -60,7 +60,7 @@ window.__ModuleLoader__.load({
|
|
|
60
60
|
builtinFallbackDisabled: '已关闭',
|
|
61
61
|
builtinFallbackBody: 'OVHcloud 匿名视觉链共 {count} 个模型,首选 {primary}。匿名限额为每 IP、每模型 2 次/分钟;5 个模型独立限额,理论合计约 10 次/分钟,实际以 OVH 限流为准。它固定在上面用户模型之后尝试,免注册、免 Key。',
|
|
62
62
|
chainLabel: '视觉后端链(给识图工具用)',
|
|
63
|
-
chainHint: '
|
|
63
|
+
chainHint: '每行可选择「设置 → 模型」中的任意可调用生成式模型;DSH 的图片能力声明只用于提示。调用时优先走该供应商已注册的 DSH adapter(包括 WebSocket / RPC / 私有协议),失败自动尝试下一行;只有明确识别为 http(s) OpenAI Chat Completions 时才会尝试 HTTP 直连兼容兜底。',
|
|
64
64
|
addFallback: '+ 添加备用视觉模型',
|
|
65
65
|
remove: '移除',
|
|
66
66
|
removeTitle: '移除这一行',
|
|
@@ -75,9 +75,12 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
catalogErrorEnvelope: '模型目录接口返回失败',
|
|
76
76
|
catalogError: '模型目录不可用(',
|
|
77
77
|
catalogFallback: '),模型字段已退回手动输入。',
|
|
78
|
-
visionCapsLoading: '
|
|
79
|
-
visionCapsError: '
|
|
80
|
-
visionCapsFiltered: '
|
|
78
|
+
visionCapsLoading: '正在读取模型图片能力声明(仅用于提示,不影响可选模型)…',
|
|
79
|
+
visionCapsError: '视觉能力元数据暂时不可用;模型仍可选择,实际可用性将由供应商已注册的 DSH adapter 在调用时验证。',
|
|
80
|
+
visionCapsFiltered: '模型能力声明只用于提示,不再作为准入条件;未声明图片能力或标成仅文本的生成式模型也可选择,调用失败会自动回退。',
|
|
81
|
+
visionCapabilityUndeclaredWarning: '⚠️ DSH 未声明此模型支持图片输入。Vision Router 会先通过该供应商已注册的适配器实际尝试;失败后自动切换到下一视觉后端。',
|
|
82
|
+
visionCapabilityTextOnlyWarning: '⚠️ DSH 将此模型标记为仅文本。仍可尝试;如果底层实际上不支持图片,调用失败后会自动切换到下一视觉后端。',
|
|
83
|
+
visionCapabilityUnknownWarning: '⚠️ 无法读取此模型的图片能力声明。仍会优先通过 DSH 已注册的适配器实际尝试;失败后自动回退。',
|
|
81
84
|
visionCapsEmptyTitle: '没有可选的用户视觉模型',
|
|
82
85
|
visionCapsEmptyBody: '检测到 {count} 个用户模型,但它们都没有被 DSH 明确标记为支持图片,因此已被安全隐藏。',
|
|
83
86
|
visionCapsHiddenPrefix: '被隐藏的模型:',
|
|
@@ -86,7 +89,7 @@ window.__ModuleLoader__.load({
|
|
|
86
89
|
visionCapsHiddenMore: '另有 {count} 个模型未显示',
|
|
87
90
|
visionCapsMissingImageHint: '如果这里有你刚在「设置 → 模型 → 添加自定义提供方」中添加的视觉模型:可以在高级设置「额外视觉模型」的下拉里选中它直接启用;或按 DSH 的方式在 $DSH_HOME/settings.yaml 为该模型补上 input: [text, image](或为整个提供方补 defaultInput: [text, image])。DSH 当前 Web 表单不会写入这个字段。',
|
|
88
91
|
visionCapsRetry: '重新检测模型',
|
|
89
|
-
chainInvalidCurrent: '
|
|
92
|
+
chainInvalidCurrent: '当前保存的后端已不在可调用模型目录中,或属于非生成式/递归路由,运行时会跳过:',
|
|
90
93
|
retryCatalog: '重试加载目录',
|
|
91
94
|
advanced: '高级设置',
|
|
92
95
|
groupTextModel: '文本模型(仅特殊场景)',
|
|
@@ -95,12 +98,12 @@ window.__ModuleLoader__.load({
|
|
|
95
98
|
'「图片轮整轮自动路由」且会话入口为视觉路由时,作为文字轮的回退模型。',
|
|
96
99
|
groupBehavior: '行为开关',
|
|
97
100
|
groupParams: '参数',
|
|
98
|
-
groupVisionOverrides: '
|
|
99
|
-
extraVisionModelsLabel: '
|
|
101
|
+
groupVisionOverrides: '视觉能力标记(可选)',
|
|
102
|
+
extraVisionModelsLabel: '额外视觉模型(仅覆盖能力标记,可选)',
|
|
100
103
|
extraVisionModelsHint:
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
+
'这个设置不再用于“解锁”下拉或允许调用;所有可调用生成式模型本来就能被选择。' +
|
|
105
|
+
'只有当你希望把某个未声明图片能力的模型明确标记为视觉模型时才需要填写。' +
|
|
106
|
+
'实际调用仍优先走 DSH adapter;只有明确的 http(s) OpenAI Chat Completions 渠道才可能使用直连兼容兜底。',
|
|
104
107
|
groupRoutes: '路由名',
|
|
105
108
|
groupProxy: '代理',
|
|
106
109
|
proxyLabel: '代理地址',
|
|
@@ -196,7 +199,7 @@ window.__ModuleLoader__.load({
|
|
|
196
199
|
wrapAllModels: '全部模型(不选 = 包装全部)',
|
|
197
200
|
addWrapper: '+ 添加手动包装',
|
|
198
201
|
textProviders: '视觉后端链',
|
|
199
|
-
textProvidersHint: '
|
|
202
|
+
textProvidersHint: '每行一个可调用生成式「provider/model」,从上到下失败回退。图片能力声明只作提示;运行时先走 DSH adapter,失败自动回退。留空清除用户覆盖。',
|
|
200
203
|
textTextProvider: '文本模型',
|
|
201
204
|
textTextProviderHint: '格式「provider/model」。',
|
|
202
205
|
presentingImage: '正在准备图片…',
|
|
@@ -234,7 +237,7 @@ window.__ModuleLoader__.load({
|
|
|
234
237
|
guidePromptNavBody: 'In the settings panel’s left navigation, click the highlighted “Plugins” entry (or press “Next”). Once that page is open, I will expand Vision Router and take you to “Vision backend chain”.',
|
|
235
238
|
guidePromptCancel: 'End guide',
|
|
236
239
|
guideChainTitle: 'Step 3 · This is the vision model',
|
|
237
|
-
guideChainBody: 'Each row
|
|
240
|
+
guideChainBody: 'Each row is a vision-backend candidate you chose and will be tried in order. Rows may all stay empty. DSH image-capability metadata is advisory only and no longer decides what you may select. The built-in OVH free chain remains the final fallback. Save when done.',
|
|
238
241
|
guideDone: 'Finish guide',
|
|
239
242
|
pending: 'Unsaved',
|
|
240
243
|
readOnly: 'The active settings provider is read-only.',
|
|
@@ -254,7 +257,7 @@ window.__ModuleLoader__.load({
|
|
|
254
257
|
builtinFallbackDisabled: 'Disabled',
|
|
255
258
|
builtinFallbackBody: 'The anonymous OVHcloud vision chain contains {count} models, starting with {primary}. Anonymous limits are 2 requests/minute per IP per model; five independent model buckets are about 10 RPM in theory, subject to OVH rate limiting. It always runs after your user models and needs no signup or API key.',
|
|
256
259
|
chainLabel: 'Vision backend chain (used by the vision tools)',
|
|
257
|
-
chainHint: 'Each row
|
|
260
|
+
chainHint: 'Each row may select any callable generative model from Settings → Models; DSH image-capability metadata is advisory only. Calls always go through the registered DSH adapter first (including WebSocket, RPC, or private transports), then fail over to the next row. The direct HTTP compatibility bridge is considered only for a positively identified http(s) OpenAI Chat Completions endpoint.',
|
|
258
261
|
addFallback: '+ Add vision fallback',
|
|
259
262
|
remove: 'Remove',
|
|
260
263
|
removeTitle: 'Remove this row',
|
|
@@ -269,9 +272,12 @@ window.__ModuleLoader__.load({
|
|
|
269
272
|
catalogErrorEnvelope: 'The model catalog endpoint failed',
|
|
270
273
|
catalogError: 'Model catalog unavailable (',
|
|
271
274
|
catalogFallback: '); model fields fell back to free-text input.',
|
|
272
|
-
visionCapsLoading: '
|
|
273
|
-
visionCapsError: 'Vision capability metadata is unavailable;
|
|
274
|
-
visionCapsFiltered: '
|
|
275
|
+
visionCapsLoading: 'Reading model image-capability metadata (advisory only; it does not hide selectable models)…',
|
|
276
|
+
visionCapsError: 'Vision capability metadata is temporarily unavailable; models remain selectable and the registered DSH adapter will verify actual support when called.',
|
|
277
|
+
visionCapsFiltered: 'Capability metadata is advisory, not an admission gate. Generative models with undeclared or text-only metadata remain selectable and failures automatically fall through.',
|
|
278
|
+
visionCapabilityUndeclaredWarning: '⚠️ DSH does not declare image input for this model. Vision Router will try the provider\'s registered adapter first and automatically fall through on failure.',
|
|
279
|
+
visionCapabilityTextOnlyWarning: '⚠️ DSH marks this model as text-only. You may still try it; if the underlying model rejects images, Vision Router automatically falls through.',
|
|
280
|
+
visionCapabilityUnknownWarning: '⚠️ Image-capability metadata is unavailable for this model. Vision Router will still try the registered DSH adapter first and fall through on failure.',
|
|
275
281
|
visionCapsEmptyTitle: 'No selectable user vision models',
|
|
276
282
|
visionCapsEmptyBody: 'DSH reported {count} user models, but none are explicitly marked as accepting images, so Vision Router hid them safely.',
|
|
277
283
|
visionCapsHiddenPrefix: 'Hidden models:',
|
|
@@ -280,7 +286,7 @@ window.__ModuleLoader__.load({
|
|
|
280
286
|
visionCapsHiddenMore: '{count} more models not shown',
|
|
281
287
|
visionCapsMissingImageHint: 'If one of these is a vision model you just added through Settings → Models → Add custom provider: select it in the “Extra vision models” dropdown under Advanced to enable it directly; or follow the DSH way and add input: [text, image] to that model in $DSH_HOME/settings.yaml (or defaultInput: [text, image] to the provider). The current DSH Web form does not write this field.',
|
|
282
288
|
visionCapsRetry: 'Re-detect models',
|
|
283
|
-
chainInvalidCurrent: 'This saved
|
|
289
|
+
chainInvalidCurrent: 'This saved backend is no longer callable, or is a non-generative/recursive route, so runtime will skip it: ',
|
|
284
290
|
retryCatalog: 'Retry catalog',
|
|
285
291
|
advanced: 'Advanced settings',
|
|
286
292
|
groupTextModel: 'Text model (special cases only)',
|
|
@@ -289,15 +295,12 @@ window.__ModuleLoader__.load({
|
|
|
289
295
|
'text-turn fallback when whole-turn routing is on and the session entry is a vision route.',
|
|
290
296
|
groupBehavior: 'Behavior',
|
|
291
297
|
groupParams: 'Parameters',
|
|
292
|
-
groupVisionOverrides: 'Vision
|
|
293
|
-
extraVisionModelsLabel: 'Extra vision models (
|
|
298
|
+
groupVisionOverrides: 'Vision capability labels (optional)',
|
|
299
|
+
extraVisionModelsLabel: 'Extra vision models (capability-label override, optional)',
|
|
294
300
|
extraVisionModelsHint:
|
|
295
|
-
'
|
|
296
|
-
'
|
|
297
|
-
'
|
|
298
|
-
'refuses images the plugin calls the channel\'s OpenAI-compatible endpoint directly. Leave empty to restore the ' +
|
|
299
|
-
'default. Well-known vision models (glm-4.6v, qwen-vl, gpt-4o, …) are recognized by name automatically and ' +
|
|
300
|
-
'need no selection here.',
|
|
301
|
+
'This setting no longer unlocks the picker or admission: every callable generative model is selectable already. ' +
|
|
302
|
+
'Use it only when you want to explicitly label an undeclared model as visual. ' +
|
|
303
|
+
'Runtime still tries the DSH adapter first; only a confirmed http(s) OpenAI Chat Completions channel may use the direct compatibility bridge.',
|
|
301
304
|
groupRoutes: 'Route names',
|
|
302
305
|
groupProxy: 'Proxy',
|
|
303
306
|
proxyLabel: 'Proxy URL',
|
|
@@ -396,7 +399,7 @@ window.__ModuleLoader__.load({
|
|
|
396
399
|
wrapAllModels: 'All models (empty = wrap all)',
|
|
397
400
|
addWrapper: '+ Add manual wrapper',
|
|
398
401
|
textProviders: 'Vision backend chain',
|
|
399
|
-
textProvidersHint: 'One
|
|
402
|
+
textProvidersHint: 'One callable generative "provider/model" per line, top-down failover. Image-capability metadata is advisory; the registered DSH adapter is tried first and failures fall through. Empty clears the override.',
|
|
400
403
|
textTextProvider: 'Text model',
|
|
401
404
|
textTextProviderHint: 'Format "provider/model".',
|
|
402
405
|
presentingImage: 'Preparing image…',
|
|
@@ -431,33 +434,39 @@ window.__ModuleLoader__.load({
|
|
|
431
434
|
function filterVisionBackendGroups(groups, capabilities) {
|
|
432
435
|
const caps = capabilities && typeof capabilities === 'object' ? capabilities : {}
|
|
433
436
|
return (Array.isArray(groups) ? groups : [])
|
|
437
|
+
.filter((group) =>
|
|
438
|
+
group &&
|
|
439
|
+
typeof group.id === 'string' &&
|
|
440
|
+
group.id !== 'vision-http' &&
|
|
441
|
+
group.id !== 'vision-chain' &&
|
|
442
|
+
!group.id.endsWith('-vision'),
|
|
443
|
+
)
|
|
434
444
|
.map((group) => {
|
|
435
|
-
const models = (
|
|
436
|
-
if (!model || typeof model.id !== 'string') return false
|
|
437
|
-
|
|
438
|
-
|
|
445
|
+
const models = (Array.isArray(group.models) ? group.models : []).filter((model) => {
|
|
446
|
+
if (!model || typeof model.id !== 'string' || model.id === '') return false
|
|
447
|
+
const capability = caps[group.id] && caps[group.id][model.id]
|
|
448
|
+
// Missing/negative image metadata is advisory. Only a positive
|
|
449
|
+
// structural rejection (non-generative endpoint, generated wrapper,
|
|
450
|
+
// missing adapter) removes an entry from the picker.
|
|
451
|
+
return !(capability && capability.attemptable === false)
|
|
439
452
|
})
|
|
440
453
|
return { ...group, models }
|
|
441
454
|
})
|
|
442
|
-
.filter((group) => group
|
|
455
|
+
.filter((group) => group.models.length > 0)
|
|
443
456
|
}
|
|
444
457
|
|
|
458
|
+
// Retained for the optional capability-label override editor. These models
|
|
459
|
+
// are no longer hidden from the backend chain; they merely lack a positive
|
|
460
|
+
// image declaration/inference and can be explicitly relabelled by experts.
|
|
445
461
|
function collectFilteredVisionBackends(groups, capabilities) {
|
|
446
462
|
const caps = capabilities && typeof capabilities === 'object' ? capabilities : {}
|
|
447
|
-
const
|
|
448
|
-
for (const group of
|
|
449
|
-
if (
|
|
450
|
-
!group ||
|
|
451
|
-
typeof group.id !== 'string' ||
|
|
452
|
-
group.id === 'vision-http' ||
|
|
453
|
-
group.id === 'vision-chain' ||
|
|
454
|
-
group.id.endsWith('-vision')
|
|
455
|
-
) continue
|
|
463
|
+
const uncertain = []
|
|
464
|
+
for (const group of filterVisionBackendGroups(groups, caps)) {
|
|
456
465
|
for (const model of Array.isArray(group.models) ? group.models : []) {
|
|
457
|
-
if (!model || typeof model.id !== 'string' || model.id === '') continue
|
|
458
466
|
const capability = caps[group.id] && caps[group.id][model.id]
|
|
467
|
+
if (capability && capability.attemptable === false) continue
|
|
459
468
|
if (capability && capability.image === true) continue
|
|
460
|
-
|
|
469
|
+
uncertain.push({
|
|
461
470
|
provider: group.id,
|
|
462
471
|
model: model.id,
|
|
463
472
|
reason: capability && typeof capability.reason === 'string' ? capability.reason : undefined,
|
|
@@ -466,7 +475,17 @@ window.__ModuleLoader__.load({
|
|
|
466
475
|
})
|
|
467
476
|
}
|
|
468
477
|
}
|
|
469
|
-
return
|
|
478
|
+
return uncertain
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function visionCapabilityWarningKey(capability, status) {
|
|
482
|
+
if (status === 'loading' || status === 'idle') return undefined
|
|
483
|
+
if (status === 'error' || !capability) return 'visionCapabilityUnknownWarning'
|
|
484
|
+
if (capability.attemptable === false || capability.image === true) return undefined
|
|
485
|
+
const modalities = Array.isArray(capability.inputModalities) ? capability.inputModalities : []
|
|
486
|
+
return modalities.length > 0 && !modalities.includes('image')
|
|
487
|
+
? 'visionCapabilityTextOnlyWarning'
|
|
488
|
+
: 'visionCapabilityUndeclaredWarning'
|
|
470
489
|
}
|
|
471
490
|
|
|
472
491
|
// ── field specs ──────────────────────────────────────────────────────────
|
|
@@ -1649,11 +1668,8 @@ window.__ModuleLoader__.load({
|
|
|
1649
1668
|
// scroll-adjacent re-renders) walks every group × model of the catalog —
|
|
1650
1669
|
// hundreds of entries — and regressed the settings card's smoothness.
|
|
1651
1670
|
const hiddenVisionBackends = useMemo(
|
|
1652
|
-
() =>
|
|
1653
|
-
|
|
1654
|
-
? collectFilteredVisionBackends(catalog.groups, visionCaps.capabilities)
|
|
1655
|
-
: [],
|
|
1656
|
-
[catalog.groups, visionCaps.status, visionCaps.capabilities],
|
|
1671
|
+
() => collectFilteredVisionBackends(catalog.groups, visionCaps.capabilities),
|
|
1672
|
+
[catalog.groups, visionCaps.capabilities],
|
|
1657
1673
|
)
|
|
1658
1674
|
// Hooks must live at the component top level — the editor below is a
|
|
1659
1675
|
// conditionally rendered plain function, so it must NOT call hooks
|
|
@@ -1931,9 +1947,6 @@ window.__ModuleLoader__.load({
|
|
|
1931
1947
|
const half = rows.some((row) => row && (row.provider ? !row.model : !!row.model))
|
|
1932
1948
|
if (half) return undefined
|
|
1933
1949
|
const filled = rows.filter((row) => row && row.provider && row.model)
|
|
1934
|
-
if (visionCaps.status === 'ready' && filled.some((row) => !visionModelVisible(row.provider, row.model))) {
|
|
1935
|
-
return undefined
|
|
1936
|
-
}
|
|
1937
1950
|
return filled.length > 0 ? { value: filled } : { clear: true }
|
|
1938
1951
|
}
|
|
1939
1952
|
const value = parseProviders(text)
|
|
@@ -2253,10 +2266,20 @@ window.__ModuleLoader__.load({
|
|
|
2253
2266
|
const chainEditor = () => {
|
|
2254
2267
|
const value = format('providers')
|
|
2255
2268
|
const rows = Array.isArray(value) && value.length > 0 ? value : [{ provider: '', model: '' }]
|
|
2256
|
-
const invalidRows =
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2269
|
+
const invalidRows = catalogReady
|
|
2270
|
+
? rows.filter((row) => row && row.provider && row.model && !visionModelVisible(row.provider, row.model))
|
|
2271
|
+
: []
|
|
2272
|
+
const advisoryRows = rows
|
|
2273
|
+
.filter((row) => row && row.provider && row.model && visionModelVisible(row.provider, row.model))
|
|
2274
|
+
.map((row) => {
|
|
2275
|
+
const capability =
|
|
2276
|
+
visionCaps.capabilities && visionCaps.capabilities[row.provider]
|
|
2277
|
+
? visionCaps.capabilities[row.provider][row.model]
|
|
2278
|
+
: undefined
|
|
2279
|
+
const warningKey = visionCapabilityWarningKey(capability, visionCaps.status)
|
|
2280
|
+
return warningKey ? { ...row, warningKey } : undefined
|
|
2281
|
+
})
|
|
2282
|
+
.filter(Boolean)
|
|
2260
2283
|
const updateChain = (index, next) => {
|
|
2261
2284
|
const list = rows.map((row) => ({ ...row }))
|
|
2262
2285
|
list[index] = next
|
|
@@ -2307,6 +2330,11 @@ window.__ModuleLoader__.load({
|
|
|
2307
2330
|
}, t('remove')),
|
|
2308
2331
|
),
|
|
2309
2332
|
),
|
|
2333
|
+
advisoryRows.map((row) =>
|
|
2334
|
+
h('p', { className: 'vr-hint vr-stealth-notice', key: `cap-${row.provider}/${row.model}` },
|
|
2335
|
+
`${row.provider}/${row.model} — ${t(row.warningKey)}`,
|
|
2336
|
+
),
|
|
2337
|
+
),
|
|
2310
2338
|
invalidRows.length > 0
|
|
2311
2339
|
? h('p', { className: 'vr-invalid' },
|
|
2312
2340
|
t('chainInvalidCurrent') + ' ' + invalidRows.map((row) => row.provider + '/' + row.model).join('、'))
|
|
@@ -2762,7 +2790,6 @@ window.__ModuleLoader__.load({
|
|
|
2762
2790
|
: visionCaps.status === 'ready'
|
|
2763
2791
|
? h('p', { className: 'vr-hint' }, t('visionCapsFiltered'))
|
|
2764
2792
|
: null,
|
|
2765
|
-
emptyVisionModelsPanel(),
|
|
2766
2793
|
catalogReady
|
|
2767
2794
|
? chainEditor()
|
|
2768
2795
|
: h('div', {
|
|
@@ -3122,10 +3149,11 @@ window.__ModuleLoader__.load({
|
|
|
3122
3149
|
}
|
|
3123
3150
|
|
|
3124
3151
|
exports.apply = apply
|
|
3125
|
-
exports.inject = ['settingsScope', 'slots', 'locale', 'sessions']
|
|
3152
|
+
exports.inject = ['settingsScope', 'slots', 'locale', 'sessions', 'connection', 'remote']
|
|
3126
3153
|
exports.unwrapModelsResult = unwrapModelsResult
|
|
3127
3154
|
exports.filterVisionBackendGroups = filterVisionBackendGroups
|
|
3128
3155
|
exports.collectFilteredVisionBackends = collectFilteredVisionBackends
|
|
3156
|
+
exports.visionCapabilityWarningKey = visionCapabilityWarningKey
|
|
3129
3157
|
exports.normalizeVisionChainRows = normalizeVisionChainRows
|
|
3130
3158
|
exports.jsonValueEqual = jsonValueEqual
|
|
3131
3159
|
exports.commitSettingsPlan = commitSettingsPlan
|
package/lib/vision-resilience.js
CHANGED
|
@@ -50,6 +50,13 @@ export const VISION_DO_NOT_RETRY_ADVICE =
|
|
|
50
50
|
'reworded question — rephrasing cannot fix an auth, rate-limit or infrastructure failure. ' +
|
|
51
51
|
'Answer from the information you already have and continue the text task; tell the user vision is temporarily unavailable.'
|
|
52
52
|
|
|
53
|
+
/** Ensure a user-visible failure sentence ends with exactly one terminal mark. */
|
|
54
|
+
export function ensureSentencePunctuation(value) {
|
|
55
|
+
const text = String(value ?? '').trim()
|
|
56
|
+
if (text === '') return ''
|
|
57
|
+
return /[.!?。!?]$/.test(text) ? text : `${text}.`
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
const AUTH_PATTERNS = [/\b401\b/, /\b403\b/, /unauthorized/i, /invalid api[ -]?key/i, /forbidden/i, /authentication/i, /check the api key/i]
|
|
54
61
|
const RATE_LIMIT_PATTERNS = [/\b429\b/, /rate.?limit/i, /quota temporarily/i, /too many requests/i]
|
|
55
62
|
const TIMEOUT_PATTERNS = [/abort/i, /timeout/i, /etimedout/i, /timed ?out/i, /deadline exceeded/i]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"sharp": "^0.35.3"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
-
"test": "node --test tests/core.test.js tests/vision-resilience.test.js tests/client.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js"
|
|
67
|
+
"test": "node --test tests/core.test.js tests/capability-advisory.test.js tests/vision-resilience.test.js tests/client.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js"
|
|
68
68
|
},
|
|
69
69
|
"pnpm": {
|
|
70
70
|
"onlyBuiltDependencies": [
|
|
@@ -76,11 +76,13 @@
|
|
|
76
76
|
"platform": "web",
|
|
77
77
|
"inject": [
|
|
78
78
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
79
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
79
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
80
|
+
"@deepseek-ai/dsh-client-connection",
|
|
81
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
80
82
|
]
|
|
81
83
|
},
|
|
82
84
|
"bundle": {
|
|
83
85
|
"patch": "./cordis.patch.yml"
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
|
-
}
|
|
88
|
+
}
|