DeepSeek Harness read_image 后会话永久 400:模型能力与路由承载判定
很多人的第一反应是「openai-responses 不支持图片」,但社区的判据更精确:核心只在模型明确声明「我不支持图片」时才降级,而视觉模型偏偏声明了支持——于是图片块原样发给一个根本承载不了它的端点,然后永久留在历史里。 一句话概括就是:能力按模型声明,承载按路由决定,这两件事对不上就是本故障(#11311)。
DeepSeek Harness 第一步:查模型声明的 input 数组
判断 DSH 会不会降级,只看模型信息里的 inputModalities 有没有 image;有就不会降级,没有才会换成文本占位。
- 打开该 provider 的模型配置,找
input或inputModalities字段。 - 如果是
[text, image],DSH 会按「支持图片」处理,图片块原样进请求。 - 如果是
[text]或字段缺失但被判为不含 image,核心会把图片替换成文本占位。 - 判断逻辑在
packages/llm/llm/src/index.ts:条件要求inputModalities !== undefined且不含 image,才调用projectImagesForTextModel。 inputModalities来自 pi-ai 模型的input数组,由适配器映射进来。
社区用录制型适配器做了反向验证:声明 ['text','image'] 时,适配器收到的就是图片块;声明 ['text'] 时,核心自己换成了占位文本。这说明降级逻辑是好的,只是没被触发。
DSH插件 第二步:分清「模型能力」与「路由承载」
同一个模型、同一个 provider,可能在一个协议上能收图、在另一个协议上完全不能,而 inputModalities 表达不了这个差异。
- 对网关做三组对照:
POST /v1/chat/completions纯文本、加公开图片 URL、加 base64 data URL。 - 社区实测结果:三种都 200,图片请求返回
image_tokens: 176。 - 再试
POST /v1/responses:纯文本连试三次全部 500bad_response_body。 - 再试
/v1/responses加图片块:400,且图片 part 被标成type:'unknown'。 - 结论:这个 provider 的 Responses 端点当前对图片不可用,Chat Completions 端点完整可用。
这一步把责任分清了:网关侧是环境事实,DSH 侧的问题是——明知道某个协议路径没有合法图片形态,仍在请求里原样塞图片块,且没有自愈。
DeepSeek Harness 第三步:确认会话是不是已经被污染
看 400 的报错位置是否随历史增长而右移,是判断「历史里有一个坏块」的最快方法。
- 看报错正文:
Invalid JSON data: Failed to deserialize the JSON body into the target type: input: data did not match any variant of untagged enum ResponseInput at line 1 column 422733。 - 记下 column 数字。社区案例三次失败分别是 422557、422664、422733,随会话尾部追加内容单调右移。
- 说明请求体是单行 JSON、体积超过 422 KB,失败点固定在图片块所在区域。
- 再看时间线:出事前该会话已成功执行 30 多次请求;第一次 400 恰好出现在
read_image的图片块首次进入历史那一步。 - 之后再发纯文本消息也 400——因为坏块还在,每条请求都会重放它。
DeepSeek Harness 三条规避路线:改协议、改声明、挂插件
按影响面从小到大排:先改协议,再考虑插件,最后才是降级模型声明。
- 改协议:把该 provider 的
api从openai-responses改成openai-completions,chat 端点文本与本地 base64 图片都能用。这是最省事、影响也最小的一条。 - 挂路由插件:
dsh plugin add github:argszero/dsh-image-route-guard#v0.1.0,在配置里写:
- id: image-route-guard
config:
routes:
- provider: agnes
model: agnes-3.0-flash
- 插件只在你点名的路由上,把图片块投影成核心自己的占位文本,输出与
textOnlyImageText逐字节一致,并且会遍历 tool_result(正是read_image返回的形态)。 - 未点名的路由、没有图片的调用、读不懂的请求,都原样透传,且是静默的。
- 降级模型声明:把
input改成[text],也让请求带占位文本——但它是全局的,同一个模型在能承载图片的路由上也会失去图片。
DeepSeek插件 为什么「删掉 image 声明」不是最优解
它会让同一个模型 id 在所有路由上都失去图片能力,而你只想修其中一条路由。
- 能力声明挂在模型条目上,所有指向这个条目的路由共用一份。
- 承载能力挂在协议端点上,同一个 provider 的两个端点可能完全不同。
- 用全局开关去修局部问题,等于用一个更大的损失换一个小修复。
- 社区的插件方案正是为这个缝隙准备的:按路由生效,其他路由不受影响。
- 插件作者也承认它有边界:它移除请求里的坏块,但不教 provider 路径学会降级,也无法自行判断哪些协议能承载图片。
DeepSeek Harness 修复方向:判据要能看见路由
真正的修法是把「这条路由能不能承载图片」这一信息带到模型信息里,让同一个降级判断按路由执行。
- 协议是适配器自己的事实,
llm-pi-ai的 catalog 已经把openai-responses映射到对应的 compat 门。 - 适配器可以带一个「该路由承载图片」的标志进模型信息。
- 同一个投影就会只在被声明为不可承载的路由上执行,不需要插件。
- 会话自愈是另一半:捕获请求体校验失败后,清理历史里的图片块再重试,或明确提示「本会话无法恢复」。
- 在那之前,插件是诚实的临时方案;等核心改了,插件就可以摘掉。
注意事项
- 别按协议下结论。同协议不同端点可能完全不同,必须实测。
- 别忽略历史。修好配置后旧会话仍然 400,需要新开会话或清理那段历史。
- 别把网关问题当 DSH 缺陷。Responses 端点对图片不可用是环境事实,DSH 的问题是没做降级与自愈。
- 插件的路由要写全。只写 provider 会覆盖该 provider 下所有模型,写 provider + model 更精确。
- 保留 column 数字。它是判断「坏块是否还在」的唯一量化线索。
如果你的模型声明本来就是 [text],那这篇不适用,去查上下文溢出或图片尺寸那条。
如果改完协议后旧会话还是 400,再确认一次 column 是否仍在右移——移动说明坏块还留在历史里,需要新开会话。相关排查见 DSH 发图片报 "url field must be a base64 encoded image"?LM Studio 转码排查。
常见问题
因为那个图片块已经写进 DeepSeek Harness 的会话历史,之后每条请求都会重放它。只要目标路由没有合法的图片形态,整条请求就会被网关拒绝,纯文本轮次也一样,会话没有自愈路径。
判据不是协议,而是模型在 DeepSeek Harness 里有没有声明图片能力。llm/src/index.ts 只在 modelInfo.inputModalities 明确不含 image 时才降级成文字;视觉模型声明了 image,就走不到降级,图片块原样发出。
能规避,但代价是全局的:在 DeepSeek Harness 里同一个模型 id 在别的能承载图片的路由上也收不到图了。能力是按模型声明的,承载是按路由决定的,改声明表达不了这个差异。
插件按你点名的路由把图片块投影成文本占位,在 DeepSeek Harness 里只影响这些路由,其他路由照常发图。安装用 dsh plugin add github:argszero/dsh-image-route-guard#v0.1.0,配置里写 routes: [{ provider, model }]。
跳过 DeepSeek Harness,直接对网关做对照:同一张图走 chat/completions 用 URL 和 base64 各试一次,再走 responses 试一次。社区案例里前两者 200 且 image_tokens 为 176,后者对纯文本都 500、对图片块标 type:'unknown'。
相关术语
- inputModalities
- inputModalities 是模型信息里声明可接受输入类型的能力字段,来自 pi-ai 模型的 input 数组。核心只在它明确不含 image 时,才把图片块降级成文本占位。— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
- projectImagesForTextModel
- projectImagesForTextModel 是把消息里的图片块替换为文本占位的原语,配合 textOnlyImageText 生成 [image: 名称 尺寸 字节] 形式。降级逻辑本身可用,问题在于它没有被触发。— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
- json_parse_error
- json_parse_error 是网关在反序列化请求体时返回的错误码,本例提示 input 不匹配 ResponseInput 的任一变体。出现它通常意味着请求体里有一个该端点不认识的块形态。— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
- 路由承载能力
- 路由承载能力是某个 provider 的具体协议端点能否接受某类内容块,与模型能力声明无关。同一个模型可能在一个协议上能收图、在另一个协议上不能。— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
来源
- #11311 — read_image 图片块在 openai-responses 协议下未做能力适配· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub