DeepSeek Harness 长截图把会话拖死:image 3000 像素上限与后续 400
一张 390×6400 的整页截图进了会话之后,用户什么也没做,下一轮、再下一轮全是同一个 400:At least one of the image dimensions exceed max allowed size for many-image requests: 3000 pixels。 这不是图片损坏,也不是这一轮上传失败——是那张图已经被写进历史,此后每次请求都会重新编码它,于是每一轮都在同一个位置被拒(#11511)。按时间线拆开看最清楚。
DeepSeek Harness 发生前:一张竖图被投影成多长
投影分两步:先按 token 网格缩放,再把长边夹到 4096;宽图会被网格压到限制内,整页竖图则卡在 4096 上。
- 第一步是 token 网格缩放,上限是
MAX_IMAGE_TOKENS = 1024。 - 第二步把长边夹到
REQUEST_IMAGE_MAX_DIMENSION。 - 社区实测的三组数据:
| 源尺寸 | 网格投影后 | 实际发送(上限 4096) |
|---|---|---|
| 1440×3900 | 755×2044 | 755×2044 |
| 520×6400 | 336×4130 | 333×4096 |
| 390×6400 | 293×4816 | 250×4096 |
- 1440×3900 这种宽图投影后远低于 3000,所以没事。
- 后两行都被夹到 4096——一个无论如何满足不了 3000 上限的数字。
这就解释了为什么故障「只在某些截图之后出现」:不是所有图都超,是整页竖图才超。
DSH插件 发生时:为什么整条请求被拒
Anthropic Messages 对多图请求的每条边限 3000 像素,且是整条请求级拒绝;只要有一张超,这一轮就没了。
- 看报错索引:
messages.9.content.88.image.source.base64.data指向的是一张历史图片,不是本轮新上传的。 - 看常量:
packages/llm/llm-deepseek/src/request-pricing.ts里REQUEST_IMAGE_MAX_DIMENSION = 4096。 - 看注释:它把 4096 称作「携带 15 张以上图片时的提供方单边限制」,而端点的 400 明确写 3000——两个数字不可能同时成立。
- 结果:适配器投影出 4096 长边,端点按 3000 判,整轮 400,这一轮的所有内容都拿不到。
社区在 master ce7073d5(比 dsh-v0.2.2-alpha.2 多 351 个提交)上复算过,clamp 确实触发了,也确实仍发出 4096——这不是估算,是代码算出来的。
DeepSeek Harness 发生后:历史图片每轮重编码,所以每轮都挂
编码器和计费器共用 resolveRequestImageTarget 这一个目标,历史里每张保留的图片在每次请求时都会被重新投影成同一尺寸。
images.ts调用resolveRequestImageTarget生成实际发送的字节。- 也就是说,不是「上传那一刻错了」,而是「每次请求都按 4096 再生成一遍」。
- 于是只要这张图还在历史里,每一轮都会带上一个 4096 长边的图片。
- 重试没用、换措辞没用,甚至这一轮完全不带新图片也没用。
- 想恢复只有两条:把那张图从历史里去掉,或者新开会话。
修复补丁把上限做成按模型声明的字段:能力条目可以写 imageMaxDimension,不写就保持 4096,所以 DeepSeek 路由与现有配置完全不受影响。声明 3000 后,390×6400 会被投影成 183×3000,请求成功。适配器包测试 176 项通过。补丁在社区分支上,未合入。
DeepSeek插件 另一条时间线:中转按 base64 计价
同一个「图片把会话拖死」的症状,还有一条完全不同的机制:网关把 base64 当普通文本计价,约 1 token/原始字节,而本地计量器只记 55~59 token/张。
- 看现象:报错是
Input exceeds the context limit (1048568 tokens),不是尺寸超限。 - 看成本差:同样 5 张图,按 Anthropic 公开公式应收约 6,860 token,该路由收了 1,001,062,差 146 倍。
- 看计量器:
estimateStructuralBlock()对 image 块只算附件引用 JSON 的长度,55~59 token/张;真实约 1,001,062,差约 3,500 倍。 - 看后果:仪表显示 surface 只有约 6 万 token,而真实已占上限 95.5%,压缩永远选不中图片。
- 再看触发范围:社区收窄后确认,膨胀只发生在「网关把 Anthropic 格式转成 OpenAI 格式、且图片在
tool_result里」时,而read_image恰好把图放在 tool_result 里。 - 对照实验:同一张 106,973 B 的 PNG,放在 tool_result 里计 93,775 token;放在 user 消息里、走另一张图时只计 1,242。
这条时间线还有个附带发现:路由没声明窗口时会取 defaultContextWindow = 262144,而上游真实上限是 1,048,568,相差 4 倍。在 profile 里给该路由声明 defaultContextWindow: 1048576 后,正在运行的会话下一次请求就切换了,不需要重启。
DeepSeek Harness 一分钟自查:先分清两条机制
看报错正文的两个关键词就够:出现 3000 pixels 是尺寸问题,出现 context limit 是计价问题。
- 复制完整报错,搜
3000 pixels,命中就是尺寸那条。 - 搜
Input exceeds the context limit,命中就是计价那条。 - 再看历史里有没有
read_image或整页截图,两类都可能出现。 - 两条同时命中的情况很少,但中转既转协议又有尺寸上限时可能叠加。
DSH插件 止损:三条按代价排序的动作
先判断是哪条机制,再按「换协议 → 换会话 → 等补丁」的顺序处理。
- 换协议或换模型:中转场景下切到
openai-responses或 Claude 路由,图片不再按 base64 计价。 - 发一条普通消息再压缩:换完路由后先发一条短消息更新
request/header,再执行/compact;直接压缩往往仍按旧 header 计算。 - 声明路由窗口:在 profile 里加
defaultContextWindow: 1048576,让仪表按真实上限判断,减少无效压缩。 - 删掉那张图:长截图场景下,把包含该图片的那条消息从历史里去掉,或新开会话。
- 等按模型声明尺寸的补丁:
imageMaxDimension字段合入后,Messages 路由声明 3000 即可根治。
注意事项
- 别只看这一轮的输入。报错索引指向历史里的第 N 张图,不是刚上传的那张。
- 别把两种 400 混为一谈。尺寸超限是
invalid_request_error,上下文超限是Input exceeds the context limit。 - 别指望 image/offload 救场。它只在 base64 字节超过 20 MiB 时触发,而真实被烧掉的是 token。
- 别用全页竖图做证据。需要截图时优先裁切或用宽幅,减少触发概率。
- 换协议后要更新 header。旧 header 里的计价与窗口会让压缩继续失灵。
如果你的 400 里带的是 messages.N.content.M 索引且提到 3000,就是第一篇的机制;如果提到 context limit 且历史里有 read_image,去查第二篇的计价。相关排查见 DeepSeek Harness 图片超过 8 张必报 400:image count limit 怎么修。
常见问题
这是 DeepSeek Harness 命中的 Anthropic Messages 端点 many-image 限制:每条边长最多 3000 像素,超一张就拒整条请求。而适配器常量 REQUEST_IMAGE_MAX_DIMENSION 仍是 4096,长截图会被压到 4096 而不是 3000,必然被拒。
因为 DeepSeek Harness 会在每次请求时把历史里保留的每张图片按同一目标尺寸重新编码。只要还有一张被投影到 3000 以上,之后每一轮都会带上它,所以表现为「一张图拖垮整个会话」。
宽的截图通常没事,DeepSeek Harness 投影后 1440×3900 会变成 755×2044,低于限制。整页竖图会出事:520×6400 投影成 336×4130 再被压到 333×4096,390×6400 同理。手机全页截图最容易命中。
不是同一个机制,DeepSeek Harness 这条 Input exceeds the context limit 是图片计价口径造成的。那是中转网关把图片的 base64 当普通文本计价(约 1 token/原始字节),而本地计量器按附件引用长度算,只记 55~59 token/张,两者差约 3500 倍,压缩永远选不中图片。
在 DeepSeek Harness 里换协议或换模型,先发一条普通消息更新 request/header,再执行 /compact。社区实测这样可以恢复;直接压缩往往失败,因为旧 header 还在按原来的计价与窗口计算。
相关术语
- many-image 请求限制
- many-image 请求限制是 Anthropic Messages 对携带多张图片的请求施加的边长上限,每条边最多 3000 像素。只要有一张超限,整个请求被拒绝,错误码为 invalid_request_error。— https://github.com/deepseek-ai/deepseek-harness/discussions/11511
- REQUEST_IMAGE_MAX_DIMENSION
- REQUEST_IMAGE_MAX_DIMENSION 是 DeepSeek 适配器里控制请求图片最长边的常量,当前值为 4096。它被注释描述为提供方的多图限制,但 Messages 端点实际只允许 3000。— https://github.com/deepseek-ai/deepseek-harness/discussions/11511
- resolveRequestImageTarget
- resolveRequestImageTarget 是把源图投影成请求目标尺寸的函数,先按 token 网格缩放,再把长边夹到上限。编码器与计费器共用同一个目标,所以历史图片每轮都会被重新投影。— https://github.com/deepseek-ai/deepseek-harness/discussions/11511
- image/offload
- image/offload 是图片卸载机制,只在累计 base64 载荷超过字节预算时触发。按 token 计价的溢出不会触发它,因此在这类路由上它从不生效。— https://github.com/deepseek-ai/deepseek-harness/discussions/11036
来源
- #11511 — a tall screenshot makes every later turn fail on Messages endpoints· deepseek-ai(GitHub Discussions)
- #11036 — 中转站按 base64 文本计价时,图片成本比 token-meter 高约 3,500 倍· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub