dsh-model-input-toggle
v1.0.0
Published
在「设置 → 模型」页面为每个模型直接开关图片输入声明(llm-pi-ai 的 input / llm-deepseek 的 inputModalities),并提供端点实际能力验证。
Readme
dsh-model-input-toggle
在 设置 → 模型 页面,为每个模型直接开关图片输入声明 —— 不用再手改 settings.yaml。
DSH 的 llm-pi-ai / llm-deepseek 适配器本来就支持 input: ["text", "image"],
但官方模型设置页只让你填 id / name / contextWindow / maxTokens。
第三方 provider 添加的模型因此默认「仅文本」,你发图片会被 DSH 直接拦下。
这个插件补上了缺失的那个开关。
⚠️ 先读这一条:声明 ≠ 能力
勾选复选框只是写入一条声明,告诉 DSH「这个模型能收图片」。 它让 DSH 不再拦截图片消息,但它不能让远端端点真的支持图片。
| | 勾选前 | 勾选后(端点其实不支持) |
|---|---|---|
| 你会看到 | DSH 本地拦截,报 UNSUPPORTED_CONTENT | 请求发出去,端点回 HTTP 400(或网关包装后的 4xx/5xx) |
| 失败发生在 | 本地 | 远端 |
失败只是换了地方,并没有消失。 所以本插件提供两种验证手段(下面第 4 节), 请务必在勾选后实际验证一次。不要因为勾上了就以为图片能用了。
功能
- 每个模型一个开关:在 provider 卡片中列出该 provider 的所有模型,逐个勾选。
- 立即写入:勾选即写入
settings.yaml,没有「保存」按钮。 - 写入后立即验证:写完后插件会通过
llm.resolveModelInfo真实读取适配器解析结果, 确认 DSH 侧已经认可这条声明(而不只是「字段写进去了」)。 - 失败自动回滚:写入失败时开关自动弹回原状态,并显示红色错误。
- 首次开启二次确认:第一次勾选会弹出风险说明,确认后不再重复打扰;取消勾选永不弹窗。
- 验证端点:每个模型一个「验证端点」按钮,真实发送一条带
image_url的请求, 并自动补发一条去掉图片的对照请求,区分「端点拒绝图片」与「这次请求本身就被拒」。结果是 ✓ 接受图片、✕ 拒绝图片、? 与图片无关、或凭据/网络/超时问题。 - 官方声明的视觉模型会锁定:由官方目录声明的模型显示为已勾选 + 禁用,标注 「该模型由官方目录声明」,不会被覆盖。
安装
插件已在 web profile 中安装并挂载。手动安装步骤如下。
方式一:从本地目录安装(推荐)
dsh plugin --profile web add C:\Users\XIAOPAN\Desktop\dsh\dsh-model-input-toggledsh.bundle.patch 会让 DSH 自动读取插件的 cordis.patch.yml,无需手改 profile 配置。
安装后需要完整重启 DSH(浏览器端的 HMR 只对已在运行的 client bundle 生效)。
方式二:手动放置
# 1. 复制到 profile 的 node_modules
$dst = "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-model-input-toggle"
Copy-Item C:\Users\XIAOPAN\Desktop\dsh\dsh-model-input-toggle $dst -Recurse -Force# 2. 在 $DSH_HOME\profiles\web\cordis.patch.yml 末尾追加
- insert:
- id: dsh-model-input-toggle
name: 'dsh-model-input-toggle'# 3. 完整重启 DSH卸载
删掉 cordis.patch.yml 中上面那段 - insert: 行,删除 node_modules\dsh-model-input-toggle
目录,然后重启 DSH。插件不会留下任何其它状态(改过的 settings.yaml 内容请自行决定是否还原)。
使用
打开 设置 → 模型。
每个 provider 卡片下方有一行折叠的「图片输入」条目,默认收起,只占一行:
▶ 图片输入 1/2 已开启这一行本身就带信息:右侧显示该 provider 下有多少模型已声明图片输入 (全部未开启时显示「全部仅文本」),所以不展开也能看出当前状态。 卡片很多时不会把页面撑长。
点这一行展开,按模型逐行列出:
▼ 图片输入 1/2 已开启 勾选即刻写入配置,无需保存。注意:这里只是声明模型可接收图片,DSH 因此 不再拦截图片消息;若端点实际不支持图片,请求会返回 400。请用「验证端点」 确认实际能力。 ☐ Buddy V4.1 Flash buddy:deepseek-v4.1-flash [验证端点] ☐ GLM 5.3 Flash buddy:glm-5.3-flash [验证端点]勾选目标模型 → 首次会弹出确认框 → 点「确认开启」→ 开关变为 ✓ 已保存。
点「验证端点」→ 等待几秒 → 看到 ✓ 或 ✕ 的结论。
展开状态是每个 provider 卡片各自独立的,且不持久化:刷新页面后 重新回到折叠状态——因为大多数时候用户只是来扫一眼,默认安静更合适。
勾选框的悬停提示:
勾选后该模型可接收图片消息,取消勾选则仅支持文本。修改后建议用带图片的请求验证端点实际能力。
界面状态说明
| 显示 | 含义 | |---|---| | ☐ 可点击 | 当前声明为仅文本 | | ☑ ✓ 已保存 | 声明已写入,且适配器已确认 | | ⟳ 正在写入… | 写入中,期间禁止重复点击 | | ✕ 红色文字 | 写入失败,开关已回滚到操作前状态 | | ☑ 禁用 + 「该模型由官方目录声明」 | 官方目录声明的模型,插件不覆盖 | | ✓ 端点接受了带图片的请求 | 端点确实支持图片 | | ✕ 端点拒绝了图片输入 | 端点拒绝图片,且去掉图片的同一请求能通过 → 建议取消勾选 | | ? 端点失败,但与图片无关 | 去掉图片的同一请求也失败 → 先排查该模型能否正常对话 | | ! 凭据 / 网络 / 超时 | 未走到图片判定,按提示修凭据或网络 |
为什么会有「与图片无关」这一档:网关经常把上游错误包成 HTTP 502 再套一层 JSON (例如
CodeBuddy 上游 HTTP 400:{"msg":"model [...] service info not found"})。 早期版本把所有 400/422 都判成「不支持图片」,会把模型未开通、网关策略、请求格式 这类问题冤枉到图片头上。现在判定前会补发一条去掉图片的对照请求: 只有「带图失败、不带图成功」才算端点拒绝图片。
截图
截图占位:请按下列五张补齐后放入
docs/,并替换本节。| 文件 | 内容 | |---|---| |
docs/00-collapsed.png| 设置 → 模型中默认折叠的一行「▶ 图片输入 1/2 已开启」 | |docs/01-panel.png| 展开后的「图片输入」区域与模型列表 | |docs/02-confirm.png| 首次勾选时的风险确认对话框(取消 / 确认开启) | |docs/03-saved.png| 勾选后的 ✓ 已保存状态 | |docs/04-verify.png| 「验证端点」的三种结论:✓ 支持、✕ 拒绝、? 与图片无关 |
验证端点(两种方式)
方式一:界面上的「验证端点」按钮
对单个模型点击即可,Host 侧发送:
POST {baseURL}/chat/completions
{
"model": "<模型 id>",
"max_tokens": 16,
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": [
{ "type": "text", "text": "Reply with the single word: ok" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,<64x64 蓝色 PNG>" } }
]}
]
}第一条必须是 system 消息。部分网关(如本机
jiuling背后的 CodeBuddy 代理) 会先校验消息顺序,不是 system 开头就直接返回 502first message is not system prompt—— 此时它根本还没看图片。早期版本漏了这条,导致所有 jiuling 验证都误报 502。
结论判定:
| 情况 | 结论 | |---|---| | HTTP 200 | ✓ 端点接受图片,确实支持视觉输入 | | 带图失败、不带图成功 | ✕ 端点拒绝图片,建议取消勾选 | | 带图失败、不带图也失败 | ? 与图片无关(模型未开通 / 网关策略 / 参数不符),先让该模型能正常对话 | | 401 / 403 | 凭据问题,换密钥后重试 | | 404 | baseURL 或协议不对 | | 超时 / 连接失败 | 网络或端点不可达 |
HTTP 200 只发一条请求;非 200 才补发对照请求,因此成功路径没有额外开销。
方式二:命令行脚本(不依赖 DSH 运行)
适合在勾选之前先验证,或放进 CI。
# 从 settings.yaml 自动读取 baseURL 与密钥引用
node tools/verify-endpoint.mjs --provider jiuling --model buddy:glm-5.3-flash
# 完全手工指定
node tools/verify-endpoint.mjs --base-url https://api.example.com/v1 --model my-vision-model --api-key sk-...
# JSON 输出(便于脚本消费)
node tools/verify-endpoint.mjs --provider agnes --model agnes-3.0-flash --json退出码:0 接受图片 · 1 拒绝图片(带图失败但不带图成功)· 2 无法判定(与图片无关 / 凭据 / 网络 / 404 / 超时)。
脚本会先关闭 undici 的 dispatcher 再退出。Windows 上若在两条请求的 socket 仍在 拆除时直接
process.exit(),会触发 libuv 断言(UV_HANDLE_CLOSING)并把退出码 变成-1073740791;现在改用process.exitCode,退出码可靠。
实测示例:
$ node tools/verify-endpoint.mjs --provider jiuling --model buddy:deepseek-v4.1-flash
✓ 端点接受了带图片的请求(HTTP 200):该模型确实支持视觉输入。
请求地址: http://47.106.84.173:8899/v1/chat/completions
$ node tools/verify-endpoint.mjs --provider jiuling --model buddy:glm-5.3-flash
? 端点返回 HTTP 502,但这与图片无关:去掉图片的同一请求仍然返回 HTTP 502,说明该端点拒绝了这次请求本身(模型未开通、网关策略、参数不符等)。请先确认该模型可正常对话,再验证图片能力。
请求地址: http://47.106.84.173:8899/v1/chat/completions
端点响应: CodeBuddy 上游 HTTP 400:model [glm-5.3-flash] service info not found第二个例子是真实的:buddy:glm-5.3-flash 在上游根本没有开通,纯文本请求也是同样的 502。
插件如实报告「与图片无关」,而不是甩一坨嵌套 JSON 或冤枉模型不支持视觉。
错误处理
| 场景 | 表现 | 你该做什么 |
|---|---|---|
| Provider 不存在 | 开关回滚,红色显示 provider "xxx" 不存在于 llm-pi-ai 配置中。 | 检查 provider 路由名是否拼错,或该 provider 是否已被删除 |
| Model 不存在(该 provider 有 models 列表时) | 开关回滚,模型 "xxx" 不在 provider "yyy" 的 models 列表中;请先在设置页添加该模型。 | 先在模型设置页添加该模型,再回来勾选 |
| 配置版本冲突 | Host 自动重读最新配置并重试一次;仍冲突则提示 配置已被其它操作修改,重试后仍然冲突。请在设置页面手动刷新后重试。 | 刷新页面后重试(通常是有另一个窗口/实例同时改了配置) |
| Host 无响应 | 10 秒后开关回滚,显示 连接超时,请检查 DSH 是否正常运行。 | 确认 DSH 进程还在;重启 DSH |
| 写入成功但适配器不认可 | 显示 配置已写入,但适配器回报该模型的可输入模态为 [...],与请求不一致。 | 通常是 provider 未激活;重启 DSH 后再看 |
| 端点实际不支持图片 | 配置写入成功,但「验证端点」返回 ✕(带图失败、去掉图片成功) | 取消勾选,或换一个真正支持视觉的模型 |
| 验证失败但与图片无关 | 「验证端点」返回 ?(去掉图片的同一请求也失败) | 先让该模型能正常对话(可能未开通/被网关拦),再验证图片 |
| 未解析到凭据 | 验证端点前就提示 未能解析凭据 "XXX":请在设置页填写该 provider 的 API 密钥。 | 在设置页填入该 provider 的 API Key |
| 密钥无效 | 验证端点返回 ! 凭据被拒绝(HTTP 401):请检查 API 密钥。 | 该密钥已失效,去 provider 后台重新签发 |
| 协议不支持自动验证 | 提示 协议 "xxx" 不支持自动验证(仅支持 openai-completions)。 | 改用带图片的真实请求手工验证 |
所有失败都会回滚到操作前的状态,不会留下「界面显示已开启、实际没写进去」的假象。
它到底改了什么
| 适配器 | 命名空间 | 写入路径 |
|---|---|---|
| llm-pi-ai | llm-pi-ai | providers.<route>.models[].input = ["text","image"] |
| llm-pi-ai(该 route 没有用户 models 列表时) | llm-pi-ai | providers.<route>.modelOverrides.<id>.input |
| llm-deepseek | llm-deepseek | models[].inputModalities = ["text","image"] |
取消勾选写回 ["text"]。
关于 modelOverrides 这条分支:适配器规定 modelOverrides 不能与 models 列表同时存在
(models 已经完整替代了目录,此时 modelOverrides 会被判为无效配置)。
所以插件只有在该 route 没有任何用户 models 列表时才走 modelOverrides;
一旦有 models 列表,就重写整个列表并保留所有其它字段。
实现注记:
settings.mutate的路径操作只能逐层进入普通对象,遇到数组即视为终点。 因此写数组元素不能使用models.0.input这类路径(那样等于把整个数组替换掉、摧毁其它条目)。 插件改为读出当前数组 → 重建整个数组 → 用一次set整体写入。
已知限制
- 位置:DSH 的
settings.models.provider-card插槽只能拿到 provider 信息(拿不到模型列表), 所以「图片输入」是渲染在 provider 卡片内的独立区域,而不是嵌在官方那一行 「上下文窗口 / 最大输出 token」下面。功能等价,位置与原需求描述有差异。 为不额外增加页面长度,该区域默认折叠,只占一行(见「使用」)。 - 卡片按 provider 分组:
llm-pi-ai的每个 route 都会渲染同一个组件实例(按settingsNs分发),组件内部按props.provider.provider区分,因此多个 provider 互不干扰。 没有settingsNs的 provider 不渲染任何界面。 - 折叠状态不持久化:刷新后回到折叠。这是有意的——避免把上一次的展开状态当成用户偏好。
- 官方参数不在本插件职责内:官方的「上下文窗口 / 最大输出 token」本来就在「编辑」表单里, 未展开编辑时并不显示;插件只负责图片输入声明,不会去折叠或改动官方字段的行为。
- 只有官方目录声明的模型被锁定;用户自己声明的模型一律可改。
- 判定「官方目录声明」依赖适配器暴露的 provider 目录信息;若适配器未提供该信息, 该 provider 的模型会按可编辑处理。
疑难排查
「验证端点」报 502 / 与图片无关
先看它说的是不是图片的事:
- ? 与图片无关 —— 去掉图片的同一请求也失败。常见原因:该模型在上游未开通
(如
model [...] service info not found)、网关策略拦截、参数不符。 先让这个模型能正常对话,再来验证图片。 - ! 凭据被拒绝(HTTP 401) —— 密钥失效。用裸请求也能复现,与本插件无关:
换密钥即可。node tools/verify-endpoint.mjs --base-url https://apihub.agnes-ai.cn/v1 --model agnes-3.0-flash --api-key sk-xxx
改了插件代码但界面/行为没变
DSH 在启动时读取 Host 代码与 Client bundle,没有热重载。改完必须:
- 把源码同步到已安装副本(
<DSH_HOME>/profiles/web/node_modules/dsh-model-input-toggle/) - 完整重启 DSH(不是刷新页面)
只改源码不同步、或同步了不重启,都会继续跑旧代码 —— 表现为「改了却一模一样」。
本机访问 3080 要账号密码
那是 dsh-basic-auth 插件在 DSH 的 HTTP server 上装的门卫,与插件的出站请求无关
(本插件的探测请求由 Host 进程直接 fetch 到你的 baseURL,不经过 DSH 的 HTTP server)。
若想绕开门卫验证端点,用上面的命令行脚本。
开发与测试
node tools/test.mjs # 全部测试(Host 24 项 + Client 18 项)
node tools/test-host.mjs # Host:配置读写、冲突重试、错误分类、信任围栏、端点探测
node tools/test-client.mjs # Client:真实渲染、折叠/展开、勾选/回滚/确认框/超时/验证按钮测试不依赖 DSH 进程:Host 测试用实现了官方 applyPathOp / mergeLayers 语义的桩服务挂载
真实的 apply(),Client 测试用带 hook 生命周期的 React 替身真实渲染 bundle 并驱动交互。
dsh-model-input-toggle/
├── lib/
│ ├── index.js Host 半:设置读写 + 端点验证 + 受围栏保护的 HTTP 路由
│ └── client.js Client 半:设置页 UI(provider-card 插槽)
├── tools/
│ ├── verify-endpoint.mjs 独立端点验证脚本(可接 CI)
│ ├── test.mjs 测试入口
│ ├── test-host.mjs Host 测试
│ ├── test-client.mjs Client 测试
│ └── settings-semantics.mjs 官方 settings 语义的测试替身
├── cordis.patch.yml bundle patch(loader 行)
├── package.json dsh.bundle.patch + dsh.client manifest
└── README.md安全说明
Host 路由 /dsh-model-input-toggle/api 只接受来自 loopback 或 trustedHosts 的请求,
并拒绝 sec-fetch-site: cross-site 与跨站 Origin。这不是身份认证,而是防止 DNS rebinding /
跨站页面借道写入你的模型配置或消耗你的端点额度。
验证端点会在请求里带上该 provider 的 API 密钥(从凭据服务或环境变量解析),
仅发往你在 settings.yaml 中配置的 baseURL。
