Skip to content

[Bug] OpenAI 线解析/重建无条件丢弃非 text/image_url 的 content part(如 DeepSeek Files API file part)——不压缩也丢图 #1205

Description

@ranxianglei

来源: #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 层没做。
  • 连带影响(本仓库):
  • 附带陷阱(验证时注意):max_tokens <= 200 的请求被判为 side request 原样透传(src/server/side-request.ts:14 SIDE_REQUEST_MAX_TOKENS = 200;src/server.ts:1705 日志 "side request … → passthrough + tag strip only, kernel state untouched")—— 用小 max_tokens 做 A/B 会得到「图片没丢」的假阴性。

建议修复(待 owner 决策;核心改动跨仓库(acp-kernel))

  1. 根修:解析/重建对不认识的 part 保持不透明 —— openaiToCore 把非 text/image_url 的 part 原样挂进 sidecar(扩展 rawOpenaiContentParts 思路,不要只塞 allImageParts() 的结果),coreToOpenai 按原顺序吐回。代码库内已有先例(Responses preamble opaque 透传)。否则任何客户端的扩展 part(Files API 引用、未来新增类型)都会静默消失。同一原则应覆盖 anthropic 未知 block 与 responses 未知 item。
  2. src/image-tokens.ts openai 分支把 type:"file" 引用计入图片 token(照 google fileData 先例)。
  3. messageImages / 占位符生成认这类 part,至少让摘要记录「这里有过一张图」。
  4. 兜底:重建时发现被丢弃的未知 part 类型打一条 warn,不要静默。

现场规避(已验证)

让图片走 inline base64 data-URL 而不是 Files API 引用,0.1.139 上能穿过(上表用例 B)。代价:每张图进请求体、不吃 Files API 的去重/复用,大图注意内联体积上限。

Activity

  1. ranxianglei commented on Sep 23, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 本 issue 的修复已提交内核 PR:**ranxianglei/acp-kernel#365 全绿)。OpenAI 线 user content 里所有非 text part(含 {type:"file"} 引用)现在进 rawOpenaiContentParts sidecar、wire 顺序保持、重建时逐字节吐回——不压缩也不再丢。单张 string-URL 图像保持 legacy sidecar 形态字节稳定。同 PR 顺带修了 #1188 的折叠期丢图(带媒体负载的消息永不被折叠)。后续:内核发布到 npm 后,本仓库跟进 acp-kernel pin bump PR 收口这两个 issue(bili CI 对 pin 版本跑 npm ci,不能预 bump 未发布版本)。

  2. ranxianglei commented on Sep 23, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已独立复现确认,根因分层清楚。本仓库侧的连带修复我会直接做 PR;kernel 根修跨仓库,按规范等 owner 拍板。

    验证(非仅凭描述)

    代码层(master 03dd9c78,acp-kernel 0.0.84,与报告一致):

    • acp-kernel/dist/wire/index.js openaiToCore() 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:14 SIDE_REQUEST_MAX_TOKENS = 200,小 max_tokens 走 passthrough,A/B 验证必须用大 max_tokens(报告者已注意,我确认了代码)。

    运行时往返复现(tsx 直驱 openaiToCore→coreToOpenai,无压缩参与):

    用例 重建后 user content
    A [text, {type:"file",file_id}] "what is in this image?\n" —— 退化为纯字符串,file part 丢(尾部 \n 正是被丢 part 在 stringContent join 时留下的空槽)
    B [text, image_url] array(text, image_url) 原样 ✅
    C [text, file, image_url] array(text, image_url),file 丢、图在 ✅

    与报告逐项吻合。结论:bug 真实存在,且与压缩无关——是解析期无条件丢弃。

    层次判断

    根因在 acp-kernel wire codec:「不认识的 part → 静默蒸发」(疏而非堵的问题——正确做法是解析期保持不透明,而不是在重建端打补丁)。本仓库两处是连带损伤:token 估算对 file 引用隐形(src/image-tokens.ts:253-256)、摘要占位符不认(src/image-note.ts),外加全程零日志。

    方案(分两段)

    第一段(本仓库,低风险,我现在做,一个 PR):

    1. src/image-tokens.ts openai 分支计入 type:"file" part,照 google fileData 先例:内联 file.file_data data-URL 按 base64 尺寸计费;opaque 引用(file_id / url)计 flat REMOTE_IMAGE_TOKENS(4096)。
    2. prepareOpenai 里 openaiToCore 之后扫一遍原始 body,把「将被 codec 丢弃的 part 类型」以 每会话一次 warn 打出(沿用 [Bug] 链检测把 bili 自己注入的 ACP 产物判成 bili→bili 链 —— 压缩内核永久跳过,长会话撞 1,048,576 上限 400(v0.1.133 回归) #1086 warnedChainSessions 的 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)。

  3. ranxianglei commented on Sep 23, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 第一段(本仓库连带修复)已完成并开 PR: #1221

    PR #1221 内容(对应 issue 修复清单第 2、4 条):

    1. src/image-tokens.ts:openai 分支计入 type:"file" part——内联 file.file_data data-URL 按 base64 尺寸计费,opaque 引用(file_id/url)计 flat 4096(照 google fileData 先例);raw-body 探测同步加 "type":"file" 标记,否则纯 file 附件 body 会被短路成 0。
    2. 新增 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 的空槽);
    • 构建后真实 proxy E2E(mock upstream、两轮重发同一历史):客户端 HTTP 200,转发体确认 file part 仍丢(kernel 未修前预期如此),warn 全程恰好一行(dedup 端到端生效);
    • 新增 tests/fix-1205-file-parts.test.ts 14 个用例钉住三种 file 形状计费、探测、检测器保留集逻辑(含非 user 角色、无 type part、退化 body)与限频逻辑;
    • 本 head 上 typecheck 干净、全量测试 2365 pass / 0 fail(2 个 gated e2e skip)、build 干净;base 即当前 master 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。

  4. ranxianglei commented on Sep 24, 2026

    @ranxianglei
    OwnerAuthor

    内核修复了吗

  5. ranxianglei commented on Sep 24, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 代码层面修了,稳定版还没发。 当前状态(刚核实):

    内核侧

    本仓库侧

    一句话:内核修复代码已就位,差一次稳定发布;你发完在这里 ping 我,我马上收口。

  6. added a commit that references this issue on Sep 24, 2026
  7. ranxianglei commented on Sep 24, 2026

    @ranxianglei
    OwnerAuthor

    内核修复了吗

    已修复并发布:acp-kernel PR #365 已合并(2026-09-24T01:25Z),npm 0.0.85 已上线(01:38Z,v0.0.84..master 只含这一个修复)。

    两条丢图路径都修了:

    • 路径 A(解析期):openaiToCore 把 user 消息所有非 text part(含 DeepSeek Files API {type:"file",file_id} 引用)收进 rawOpenaiContentParts sidecar,coreToOpenai 按 wire 顺序逐字节吐回;单张 string-URL 图保持 legacy 单数 sidecar(字节稳定)。
    • 路径 B(折叠期):带媒体负载的消息永不被折叠。

    激活 PR(本仓库 pin 0.0.84 → 0.0.85 + warn 收敛 + 往返/e2e 测试):**https://github.com/ranxianglei/billion-context/pull/1241**,合并发版后即可在 dsh + DeepSeek 实测验证。

  8. added a commit that references this issue on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp1

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions