DSH 官方模型图像输入"不支持"问题排查与修复
摘要 :dsh 0.1.5-rc.1 中,手写进
settings.yaml的官方模型条目默认是纯文本的,缺少inputModalities: [text, image]声明会导致发图时被 harness 在请求发出前直接拒绝(UNSUPPORTED_CONTENT)。根因是llm-deepseek.models一旦出现即整体替换内置目录(非按 id 合并),手写条目未声明该字段时 schema 缺省为["text"]。修复只需在模型条目中补一行inputModalities: [text, image],无需装插件、换模型或重启;实测 headless 冷启动端到端验证通过。文章还梳理了三个常见误区(字段名写错、网上插件方案不可靠、models 列表是整体替换而非补充)及 catalog 字段参考表。
文章目录
- [DSH 官方模型图像输入"不支持"问题排查与修复](#DSH 官方模型图像输入"不支持"问题排查与修复)
环境:dsh 0.1.5-rc.1(npm 全局安装,
~/.local前缀)· Ubuntu · 官方路由deepseek-official / deepseek-flash
修复方案
- dsh(DeepSeek Harness)中手写进配置的模型条目默认是纯文本的 。官方路由
llm-deepseek的模型目录(catalog)条目如果不声明inputModalities: [text, image],发图时 harness 会在请求发出前直接拒绝(UNSUPPORTED_CONTENT)。在~/.dsh/settings.yaml的模型条目里补一行即可修复:
yaml
llm-deepseek:
models:
- id: deepseek-flash
name: deepseek-flash
contextWindow: 1000000
inputModalities: [text, image] # ← 关键就是这一行
- 不需要装任何插件,不需要换模型,不需要重启(官方文档:模型变更下次请求即生效;实测 headless 冷启动验证通过)。
现象
- 把 dsh 从 0.1.2 升级到 0.1.5-rc.1 后,Web UI 中通过官方渠道(
agent-default-model: provider: deepseek-official, model: deepseek-flash)发送图片,提示"不支持"。同一个deepseek-flash模型本身是原生多模态的(DeepSeek-V4.1-Flash),问题显然出在 harness 侧。
当时配置长这样:
yaml
llm-deepseek:
models:
- id: deepseek-flash
name: deepseek-flash
contextWindow: 1000000
- 而同一个
settings.yaml里,自定义 provider(llm-pi-ai.providers.new-api)下的模型都声明了input: [text, image]且能正常发图。
排查过程
1. 排除配置文件语法问题
dsh --profile web --dump-config一直报failed to parse: value expected,一度怀疑 settings.yaml 被运行时写坏。用 Python YAML 逐个校验settings.yaml、cordis.patch.yml均合法。最后发现是乌龙 :--dump-config输出的是 YAML 而不是 JSON,之前用jq去解析才报的错。输出里还带!!js dshHomePath(...)这类自定义 tag,是 cordis 的运行时表达式,属于正常现象。
教训:先看命令的原始输出和退出码,再怀疑配置本身。
2. dump 看不到模型目录
--dump-config输出的是插件注册树(patch layers),llm-deepseek插件没有 config 块,模型目录不在这里。真正的逻辑在依赖包里:
bash
node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/index.js
3. 读源码,三个关键证据
- 证据一:拒绝发生在 harness 内部,读的是
inputModalities
js
// L1620 --- 发图前的门禁
if (connection.models.find((entry) => entry.id === options.model)
?.inputModalities?.includes("image") !== true)
throw new LlmError(`DeepSeek model "${options.model}" does not accept image input.`, "UNSUPPORTED_CONTENT");
证据二:catalog 条目的 schema,inputModalities 缺省值是纯文本
js
// L1877-1879 --- catalogModel zod schema(节选)
contextWindow: z.number().step(1).min(1),
maxTokens: z.number().step(1).min(1),
inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"]),
// MODEL_MODALITIES = ["text", "image"]
证据三:插件自带的默认目录本来就有图像支持,但 models 是整体覆盖不是合并
js
// L1843-1846 --- DEFAULT_MODELS(节选)
{
id: "deepseek-flash",
name: "DeepSeek-V41-Flash",
contextWindow: DEFAULT_CONTEXT_WINDOW, // 1e6
inputModalities: ["text", "image"],
imagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, // 64e4
imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES, // 1 MiB
systemPromptUpdate: "in-history"
},
js
// L1896 --- models 缺省用 DEFAULT_MODELS;一旦手写,整个目录被替换
models: z.array(catalogModel).default(DEFAULT_MODELS),
根因
llm-deepseek.models一旦出现在 settings.yaml,整个内置目录被这条列表替换 (不是按 id 合并)。手写的deepseek-flash条目没有inputModalities,schema 缺省填["text"],于是 L1620 的门禁在发图前直接拒绝,模型本身的能力根本没被问到。- 另外两个字段其实都是冗余的:
contextWindow: 1000000等于默认值DEFAULT_CONTEXT_WINDOW = 1e6;name只影响显示。真正的信息损失只有inputModalities(连带imagePixelBudget/imageMaxBytes,不过这两个在图像模型上不声明时会回退到同一组默认值,行为等价)。
修复
- 按惯例先备份(
~/.dsh/里的.bak-*命名传统),再做最小增量修改:
bash
cp -p ~/.dsh/settings.yaml ~/.dsh/settings.yaml.bak-before-image-input
- 在条目中加一行
inputModalities: [text, image]。字段顺序无关,其余段(ui-onboarding、pet、live-stats 等运行时状态)完全不动。
验证
- 静态 :YAML 解析通过;
dsh --profile web --dump-config退出码 0。 - 端到端 :用 headless profile 做一次性任务,走的正是官方
deepseek-official/deepseek-flash路由:
bash
python3 -c "from PIL import Image; Image.new('RGB',(16,16),(255,0,0)).save('/tmp/smoke.png')"
dsh --profile headless "Use the file read tool to view /tmp/smoke.png, then reply with only the dominant color."
# → Red
- 图像经工具链进入模型消息、通过门禁、被正确识别。修复前这条路径会抛
does not accept image input。 - 关于运行中的 web 实例 :官方文档说明模型变更下次请求生效、无需重启。如果 Web UI 仍提示不支持(实例启动于修改前),重启
dsh web即可。
三个常见误区(都是踩过的坑或差点踩的坑)
- 字段名写成
input。input: [text, image]是llm-pi-ai自定义 provider 的写法;官方路由llm-deepseek用的是inputModalities。两者 schema 不同源,写错了不会被读取(问题原样保留),静默失败最迷惑人。 - 网上搜到的插件方案 。实测
dsh-paste-input在 npm 上根本不存在(404);dsh-attach-picker存在但没有必要------上传/粘贴/拖拽链路是内置的,卡点只在模型声明。搜到"装插件 + 硬刷新"之类方案时,先确认包名是否真实存在。 - 以为 models 列表是"补充" 。它是整体替换。手写一条
deepseek-flash会把内置的deepseek-v4-flash、deepseek-v4-pro、deepseek-v4-flash-vision-exp全部挤掉。如果只想微调官方模型,替换列表时要自己把需要的能力字段带全;如果没有任何特殊需求,直接删掉models覆盖、用内置目录是最稳的。
附:llm-deepseek catalog 模型条目字段参考
来自 catalogModel schema 与 resolveModels 校验逻辑:
| 字段 | 类型 | 缺省 | 说明 |
|---|---|---|---|
id |
string | 必填 | 非空;列表内不可重复 |
name |
string | 无 | 显示名(内置 flash 为 "DeepSeek-V41-Flash") |
contextWindow |
正整数 | 1000000 |
上下文窗口 |
maxTokens |
正整数 | 内置默认 | 单次最大输出 |
inputModalities |
["text"] / ["text","image"] |
["text"] |
图像输入开关;非空、无重复、仅许 text/image |
imagePixelBudget |
"low" 或正整数 |
640000 |
单请求像素预算;仅图像模型可声明 |
imageMaxBytes |
正整数 | 1048576 |
单请求图像字节上限;仅图像模型可声明 |
systemPromptUpdate |
"in-history" |
无 | 目前唯一合法值 |
校验细节:纯文本模型声明图像限额会直接报错;imageDetail 字段已废弃(改用 imagePixelBudget)。