Repository navigation
[Bug] OpenAI 线解析/重建无条件丢弃非 text/image_url 的 content part(如 DeepSeek Files API file part)——不压缩也丢图 #1205
Description
来源: #1188 (#1188) 分析 压缩后图像丢失 时发现
问题
OpenAI chat wire 上,bili 的解析→重建往返会无条件、静默丢弃 user 消息中所有 type 既非 text 也非 image_url 的 content part —— 与压缩/折叠/保护窗口完全无关,新会话第一条消息就丢。
这直接命中 dsh native + DeepSeek 的默认附件流:附件先走 Files API 上传,请求体里是 {"type":"file","file_id":"file-api-…"}(上传失败才回退 inline base64)。经过 bili 后模型只能看到文本、拿不到像素 → 无法识图(#1188 中 @terryvvan 报告的表面现象)。#1188 有两条独立的丢图路径(本条解析期丢失 + 折叠吞掉带图消息),两条都修了 dsh 侧才算通。
复现(已独立验证,master 03dd9c78 / bili 0.1.139 / acp-kernel 0.0.84)
最小会话形状(2 条短消息 + 1 条待测消息),max_tokens=8000,日志确认全程 nudge idle … max compressible 0(没有任何压缩发生),断言实际转发给上游的 body:
| 用例 | 最终转发的 user content | file part | base64 像素 |
|---|---|---|---|
A [text, {type:"file",file_id}] |
退化成纯字符串 "\nwhat is in this image?\n" |
❌ 丢 | — |
B [text, {type:"image_url", data-URL}] |
array(text,image_url) 原样 |
— | ✅ 在 |
C [text, file, image_url] |
array(text,image_url) |
❌ 丢 | ✅ 在 |
报告者 @CChusky 另验证:npm 0.1.140 tarball 的 openaiToCore / allImageParts / stringContent / stripHistoricalImages / parseDataUrl 与 0.1.139 逐字节相同 → 「升级到最新版」对这条路径无效(0.1.140 上同样复现)。
根因(代码定位)
核心全在 acp-kernel wire codec(acp-kernel/dist/wire/index.js):
openaiToCore()user 分支(:233):const text = stringContent(m.content)+const imgs = allImageParts(m.content)stringContent()(:423)只取type === "text"的 part;allImageParts()(:431)只认type === "image_url"且要求 string url;{type:"file"}两边都不认 → 既不进 text 也不进 sidecar,直接蒸发;
coreToOpenai()user 分支按text+rawOpenaiContent(Parts)重建 content —— 上一步没留下,这里无从还原。- 同类盲区(未实测,待单独验证):
anthropicToCore()的 switch 没有 default 分支,未知 block type 静默 fall-through 丢失;- Responses wire 已有显式 opaque 机制(
isOpaqueItem()/OPAQUE_ITEM_TYPES→preamble透传,:449/:513),但只覆盖白名单 item 类型 —— 说明「不透明透传」这个设计模式代码库里已经存在,只是 OpenAI content-part 层没做。
- 连带影响(本仓库):
src/image-tokens.ts:253-256openai 分支只计image_url→ file 引用对 token 估算隐形(google 分支已有fileData.fileUri先例,:226-227;相关讨论见 Design discussion: should localInputEstimate count images by BILLING mode (compress triggers move) or by context-window pressure? #1118);src/image-note.tsmessageImages()/imagePlaceholders()不认这类 part → 摘要里连[image: …]占位符都不会出现(「这张图存在过」不可见);
- 附带陷阱(验证时注意):
max_tokens <= 200的请求被判为 side request 原样透传(src/server/side-request.ts:14SIDE_REQUEST_MAX_TOKENS = 200;src/server.ts:1705日志 "side request … → passthrough + tag strip only, kernel state untouched")—— 用小 max_tokens 做 A/B 会得到「图片没丢」的假阴性。
建议修复(待 owner 决策;核心改动跨仓库(acp-kernel))
- 根修:解析/重建对不认识的 part 保持不透明 ——
openaiToCore把非text/image_url的 part 原样挂进 sidecar(扩展rawOpenaiContentParts思路,不要只塞allImageParts()的结果),coreToOpenai按原顺序吐回。代码库内已有先例(Responsespreambleopaque 透传)。否则任何客户端的扩展 part(Files API 引用、未来新增类型)都会静默消失。同一原则应覆盖 anthropic 未知 block 与 responses 未知 item。 src/image-tokens.tsopenai 分支把type:"file"引用计入图片 token(照 googlefileData先例)。messageImages/ 占位符生成认这类 part,至少让摘要记录「这里有过一张图」。- 兜底:重建时发现被丢弃的未知 part 类型打一条 warn,不要静默。
现场规避(已验证)
让图片走 inline base64 data-URL 而不是 Files API 引用,0.1.139 上能穿过(上表用例 B)。代价:每张图进请求体、不吃 Files API 的去重/复用,大图注意内联体积上限。
Activity
ranxianglei commentedon Sep 23, 2026
[bot] 🏷 本 issue 的修复已提交内核 PR:**ranxianglei/acp-kernel#365 全绿)。OpenAI 线 user content 里所有非
textpart(含{type:"file"}引用)现在进rawOpenaiContentPartssidecar、wire 顺序保持、重建时逐字节吐回——不压缩也不再丢。单张 string-URL 图像保持 legacy sidecar 形态字节稳定。同 PR 顺带修了 #1188 的折叠期丢图(带媒体负载的消息永不被折叠)。后续:内核发布到 npm 后,本仓库跟进 acp-kernel pin bump PR 收口这两个 issue(bili CI 对 pin 版本跑npm ci,不能预 bump 未发布版本)。ranxianglei commentedon Sep 23, 2026
[bot] 🏷 已独立复现确认,根因分层清楚。本仓库侧的连带修复我会直接做 PR;kernel 根修跨仓库,按规范等 owner 拍板。
验证(非仅凭描述)
代码层(master
03dd9c78,acp-kernel 0.0.84,与报告一致):acp-kernel/dist/wire/index.jsopenaiToCore()user 分支(:250–270):只取stringContent()(:423,仅type==="text") +allImageParts()(:431,仅type==="image_url"且 string url)。{type:"file"}两边都不收 → 蒸发;coreToOpenai()user 分支(:378–392)按 text +rawOpenaiContent(Parts)重建,无从还原。anthropicToCore的 switch 无 default 分支(text/tool_use/tool_result/thinking/image 之外静默 fall-through);Responses 已有白名单 opaque 机制(OPAQUE_ITEM_TYPES={additional_tools,mcp_list_tools}:445–451 →preamble透传 :513)——「不透明透传」模式代码库已有先例,只是 OpenAI content-part 层没做。src/server/side-request.ts:14SIDE_REQUEST_MAX_TOKENS = 200,小 max_tokens 走 passthrough,A/B 验证必须用大 max_tokens(报告者已注意,我确认了代码)。运行时往返复现(tsx 直驱
openaiToCore→coreToOpenai,无压缩参与):[text, {type:"file",file_id}]"what is in this image?\n"—— 退化为纯字符串,file part 丢(尾部\n正是被丢 part 在stringContentjoin 时留下的空槽)[text, image_url][text, file, image_url]与报告逐项吻合。结论:bug 真实存在,且与压缩无关——是解析期无条件丢弃。
层次判断
根因在 acp-kernel wire codec:「不认识的 part → 静默蒸发」(疏而非堵的问题——正确做法是解析期保持不透明,而不是在重建端打补丁)。本仓库两处是连带损伤:token 估算对 file 引用隐形(
src/image-tokens.ts:253-256)、摘要占位符不认(src/image-note.ts),外加全程零日志。方案(分两段)
第一段(本仓库,低风险,我现在做,一个 PR):
src/image-tokens.tsopenai 分支计入type:"file"part,照 googlefileData先例:内联file.file_datadata-URL 按 base64 尺寸计费;opaque 引用(file_id/url)计 flatREMOTE_IMAGE_TOKENS(4096)。prepareOpenai里openaiToCore之后扫一遍原始 body,把「将被 codec 丢弃的 part 类型」以 每会话一次 warn 打出(沿用 [Bug] 链检测把 bili 自己注入的 ACP 产物判成 bili→bili 链 —— 压缩内核永久跳过,长会话撞 1,048,576 上限 400(v0.1.133 回归) #1086warnedChainSessions的 bounded-FIFO 惯例)——治标不治根,但让静默变响亮,kernel 修复落地前用户至少能在日志里看到附件丢了。第二段(跨仓库,待 owner 拍板):
kernel 根修:
openaiToCore把非 text/image_url 的 part 原样挂进 sidecar(扩展rawOpenaiContentParts思路,保持原顺序),coreToOpenai吐回;同一原则覆盖 anthropic 未知 block 与 responses 未知 item。这是 wire/message-shape 变更,按 AGENTS.md §5 需先发布 acp-kernel 再 bump pin;src/image-note.ts的占位符识别依赖该 sidecar 字段,随第二段补。我的建议:批准 issue 里的方案 1(opaque sidecar),它与 Responses preamble 既有模式一致,是最小架构动作。 请 @ranxianglei 确认后我在 acp-kernel 开工。行为变更披露(第一段)
token 估算开始为 file part 计费后,含 Files API 附件的会话估算值 旧值 → 新值:每个 file 引用 +4096。kernel 修复前该 part 实际未转发,属有界保守高估(与 remote URL 同策略);修复落地后即变为精确值。方向安全(高估只会更保守地触发压缩,不会放行超窗 payload)。
ranxianglei commentedon Sep 23, 2026
[bot] 🏷 第一段(本仓库连带修复)已完成并开 PR: #1221
PR #1221 内容(对应 issue 修复清单第 2、4 条):
src/image-tokens.ts:openai 分支计入type:"file"part——内联file.file_datadata-URL 按 base64 尺寸计费,opaque 引用(file_id/url)计 flat 4096(照 googlefileData先例);raw-body 探测同步加"type":"file"标记,否则纯 file 附件 body 会被短路成 0。src/wire-drop-warn.ts(纯函数检测器)+prepareOpenai在openaiToCore之后调用:把将被 codec 丢弃的 part 类型以每会话每类型集一次 warn 打出(bounded FIFO,沿用 [Bug] 链检测把 bili 自己注入的 ACP 产物判成 bili→bili 链 —— 压缩内核永久跳过,长会话撞 1,048,576 上限 400(v0.1.133 回归) #1086 惯例)。静默丢件从此在日志里可见。验证证据(非「应该能行」):
[text,file]→ 退化为纯字符串,尾部\n是被丢 part 的空槽);tests/fix-1205-file-parts.test.ts14 个用例钉住三种 file 形状计费、探测、检测器保留集逻辑(含非 user 角色、无 type part、退化 body)与限频逻辑;03dd9c78,无陈旧基座问题。行为变更披露:含 Files API 附件的请求 token 估算每个 opaque file 引用 +4096。kernel 修复前该 part 实际未转发,属有界保守高估;kernel 修复落地后即为精确值。方向安全(只会让 fit/compress 更保守)。新增一条 warn 日志(限频)。无 config schema / persistence / wire shape 变更。
PR 不关闭本 issue:真正的丢件发生在 acp-kernel wire codec,opaque sidecar 根修(修复清单第 1 条)是跨仓库的 wire/message-shape 变更,按规范等你的拍板。你确认后我在 acp-kernel 开工(先发布 kernel、再 bump pin);
src/image-note.ts占位符识别(第 3 条)依赖该 sidecar 字段,随根修一起补。另提醒:从本 issue 派生的跨仓库工作按 loop guard 只留在这里等决定,不再另开 issue。fix: count and warn about OpenAI file parts dropped by the wire codec (…
ranxianglei commentedon Sep 24, 2026
内核修复了吗
ranxianglei commentedon Sep 24, 2026
[bot] 🏷 代码层面修了,稳定版还没发。 当前状态(刚核实):
内核侧
{type:"file"})进rawOpenaiContentPartssidecar、wire 顺序保持、重建逐字节吐回;同 PR 含 这个插件压缩上下文,会导致deepseek识图出问题。 #1188 的折叠保护(带媒体负载的消息永不被折叠)。latest仍是 0.0.84,不含该修复。目前只有 per-PR 预发布构建(pr-365→0.0.84-pr.365.231)。kernel 仓库还有几个在途 PR(persist EPERM 根治(#362 follow-up):可诊断告警 + Windows AV 排除文档 #367/fix: break the small-model same-parameter compress failure loop early (#156) #370/chore: bump acp-kernel to 0.0.47 (Layer 1 spill/retry/throttle, #374) #375/release v0.1.66 #376,release v0.1.66 #376 已按 0.0.85 基线构建)——现在切 0.0.85 只带已合入的内容,等它们落地可一并发。本仓库侧
92cbf983,diff 完好 4 files/+219),CI 全绿(ubuntu/windows × node 22/24、零 token e2e、build、version-guard 全 success)——它不依赖内核发布,可随时合并。npm ci,不能预 bump 未发布版本)+ [Bug] OpenAI 线解析/重建无条件丢弃非 text/image_url 的 content part(如 DeepSeek Files APIfilepart)——不压缩也丢图 #1205 第 3 条 image-note 占位符识别(依赖新 sidecar 字段)。两步做完关 [Bug] OpenAI 线解析/重建无条件丢弃非 text/image_url 的 content part(如 DeepSeek Files APIfilepart)——不压缩也丢图 #1205/这个插件压缩上下文,会导致deepseek识图出问题。 #1188。一句话:内核修复代码已就位,差一次稳定发布;你发完在这里 ping 我,我马上收口。
Merge pull request #1221 from ranxianglei/2026-09-24_fix-1205-file-pa…
ranxianglei commentedon Sep 24, 2026
已修复并发布:acp-kernel PR #365 已合并(2026-09-24T01:25Z),npm 0.0.85 已上线(01:38Z,
v0.0.84..master只含这一个修复)。两条丢图路径都修了:
openaiToCore把 user 消息所有非 text part(含 DeepSeek Files API{type:"file",file_id}引用)收进rawOpenaiContentPartssidecar,coreToOpenai按 wire 顺序逐字节吐回;单张 string-URL 图保持 legacy 单数 sidecar(字节稳定)。激活 PR(本仓库 pin 0.0.84 → 0.0.85 + warn 收敛 + 往返/e2e 测试):**https://github.com/ranxianglei/billion-context/pull/1241**,合并发版后即可在 dsh + DeepSeek 实测验证。
Merge pull request #1241 from ranxianglei/2026-09-24_kernel-0.0.85-pin
chore: pin acp-kernel 0.0.85 — activate media-payload preservation (r…
kernel: treat Google wire fileData/inlineData URL refs as media payloads
kernel: keep trailing opaque parts when flushing leading ones (#3658 …