Skip to content

多模态会话上下文控制失效 + preflight 救援 502(codex responses store:false) #488

Closed

Description

@Rika-xie

场景:codex 会话(model gpt-5.6-luna,窗口 1.05M),用户消息含 7 张本地截图路径(codex 将其加载为多模态图片内容后进入请求)。请求 payload 涨到 ~1.49M tokens,超过窗口,所有请求 502 卡死。

A. 多模态(图片)token 统计失真 → 上下文控制失效

同一请求两个口径相差 15 倍:

nudge idle: usage=9% (97814/1050000)     ← bili 统计:97K tokens
context 1493817 tokens exceeds 1050000    ← preflight 检测:1.49M tokens

推断:usage 只统计文本 token,图片/多模态内容不计入 → nudge/压缩基于错误的 9% 判断从不触发 → payload 被图片撑破窗口无人拦截。影响所有含图片的多模态会话。相关 #330 / #333(超窗后 rescue 的另一面)。

B. preflight 摘要调用被上游 400 拒绝(store:false)

超窗后 preflight(#254 机制)启动,构造摘要请求发往 /codex/responses,上游返回 HTTP 400 {"detail":"Store must be set to false"} → 摘要拿不到 → 无法压缩 → 超窗 payload 不转发 → 502。

store:false 是 codex Responses API 的参数约束,生态里其他 codex 代理处理过同类问题(Responses 连接需保留 store:false,如 diegosouzapw/OmniRoute#11826)。

已核查 v0.1.75 未覆盖以上两个问题。相关 #333。

Activity

  1. ranxianglei commented on Sep 3, 2026

    @ranxianglei
    Owner

    🤖 ework agent · qwen3.8-27b

    [bot] 🏷 开始处理 #488。两个问题都已核实存在,根因层次已定位,先报结论:

    B. preflight 摘要调用 400(store:false)— 确认,修复明确

    src/preflight.ts:148 的 summaryPayload() responses 分支构造的是 {model, max_output_tokens, instructions, input, stream:false} —— 没有 store 字段。codex relay 要求 Responses 调用带 store:false → 400 → 摘要拿不到 → fail-fast 502。

    修复:preflight 的摘要请求加 store:false(仅 responses 协议;chat completions 没有该参数,加了反而可能破坏其他 provider)。主转发路径不受影响(codex 原始 body 原样透传,自带 store:false);compress-loop 重发复用客户端原始 body(spread),同样不受影响。所以改动面只有 preflight 这一处合成请求。

    A. 多模态 token 统计失真 — 确认,但根因比「usage 只统计文本」深一层

    分层分析:

    1. nudge 用的不是文本统计。nudge 的 tokenCount 是上游上一轮回报的 usage.input_tokens(含图片),只是滞后一轮——你看到的 9% 是图片加入前的基线,不是「从不触发」。
    2. 真正的缺陷在 bili 自己的 payload 尺寸模型。estimateCoreMessages(src/preflight.ts:90)只数消息文本;而图片字节实际是完整转发的(wire codec 把图片放进 sidecar:imageBase64 / rawResponsesItem / rawAnthropicBlock,转发时原样还原)。于是 preflight 的 fit 判定(result.payloadEstimate < limit,server.ts:2248)系统性低估 ~15×:误判「已低于窗口」→ 把超窗 payload 当安全放行 → 上游 400 → 死循环。这才是「上下文控制失效」的准确含义——不是压缩不触发,而是触发后判断失真、甚至假成功。
    3. 附带缺陷:self-heal 从被拒 payload 学窗口(server.ts:2491)时学的也是纯文本数字,会把窗口学歪。

    修复方案(host 侧,不动 acp-kernel)

    • 新增图像 token 核算:扫描原始请求体里的图像 part。base64 data URL → base64长度/4(与 bili 文本估算同一 chars/4 规则;也与你这个 relay 的实际计费吻合——7 张截图 ≈ 1.4M tokens 正是 base64 字符量级;若按 OpenAI 官方像素 tile 算法,7 张图最多 ~11K tokens,对不上你看到的 1.49M,可佐证该 relay 按字节计费);远程 URL(看不到字节)→ 固定保守值 4096。
    • 该成本计入 preflight 触发 / fit 判定 / self-heal 学习。图片单独就超窗时,fail-fast 会明确说「图片本身占 ~X tokens,压缩无法移除——需缩小/移除图片或调大窗口」,而不是假成功放行。
    • 取舍需要你拍板:默认按字节计(chars/4)匹配 codex relay 生态的计费行为;对按像素 tile 计费的官方 Anthropic/OpenAI 用户这会高估、导致压缩偏早触发。我加了环境变量 BILI_IMAGE_TOKEN_CAP(单图上限)供此类用户调低。若你认为默认方向应反过来(tile 优先 + 字节兜底),merge 前告诉我即可调整。

    正在实现,PR 链接稍后更新在本 issue。

  2. ranxianglei commented on Sep 3, 2026

    @ranxianglei
    Owner

    🤖 ework agent · qwen3.8-27b

    [bot] 🏷 修复已完成,PR 已开:**https://github.com/ranxianglei/billion-context/pull/489**(分支 2026-09-03_multimodal-image-tokens-store-false,commit acd9dce)

    两个根因的落地修法

    • B(store:false):src/preflight.ts 的 summaryPayload responses 分支补上 store: false。只影响 preflight 自己合成的摘要调用——主转发路径透传 codex 原始 body(自带 store:false)、compress-loop 重发是 {...requestBody} 展开,均不受影响。
    • A(图片 token 统计):新增 src/image-tokens.ts,从原始 wire body 直接统计图片份额(不依赖 kernel sidecar,也绕开了 kernel 对远程 URL 图片不捕获的既有缺陷),加进三处尺寸决策:preflight 触发判断、preflight 循环内每轮 fit 判断(新增 imageFloor,折叠文本永远无法把图片字节"折没",杜绝"假成功→转发→400→死循环")、超窗自学习窗口。已知 base64 按 ceil(b64长度/4) 计(与 defaultCountTokens 的 chars/4 同规则,且符合你 relay 的实际计费——7 张截图 ≈1.4M tokens 排除了像素 tile 计费,后者上限只有 ~11K);无法测尺寸的远程 URL 按固定 4096 计。无图片的请求走字符串嗅探门,连 JSON.parse 都不会执行,纯文本会话零开销、零行为变化。
    • 当仅图片就超过窗口时,fail-fast 错误信息会明确写出:Images alone account for ~N tokens (≥ window M); compression cannot remove them — shrink or remove the images, or raise the window.(你的场景:7 图 ~1.39M > 1.05M → 立刻得到可操作结论,而不是卡死。)

    验证

    • npm run typecheck ✅
    • npm test ✅ 952/953 —— 唯一失败是 launcher.test.ts 的 codex 路径解析用例,在干净 master 上同样失败(本沙箱装了 /usr/bin/codex),环境依赖,与本改动无关
    • npm run build ✅
    • 新增 6 个测试(tests/fix-488-multimodal.test.ts):估算器单测 ×4(三协议 data URL / 远程 URL / 多图求和 / 解析门 / cap 钳制)+ e2e 回归 ×2:(B) 超窗恢复会话的 preflight 摘要请求确实带 store:false 且成功转发 200;(A) 7×15K token 图片 vs 10K 窗口 → 502 preflight_compress_failed + 上述 actionable 提示,上游零转发
    • plugin/proxy 双模式均覆盖(preflight 无条件运行,store:false 修的是 proxy 合成的摘要调用,图片统计与模式无关)

    合并前留给你拍板的一点:默认计费模型取 b64 长度/4(匹配你报告里 relay 的行为)。如果你的上游是按像素 tile 计费(官方 Anthropic/OpenAI 那种),请设环境变量 BILI_IMAGE_TOKEN_CAP=<每图上限> 钳制每图成本——不设的话这类上游会偏早触发压缩,但方向安全(宁早勿漏)。如无异议即可合并;按仓库规则我不会自行 merge。

  3. added 2 commits that reference this issue on Sep 3, 2026
  4. ranxianglei commented on Sep 3, 2026

    @ranxianglei
    Owner

    收尾:#489 已合并,随 v0.1.77 发布(release PR #497,合并后 CI 自动 publish)。两个根因(store:false、图片计入尺寸决策)均经第三方独立 review + 全量 codex e2e 鐘452s)验证。残留设计风险(b64/4 默认计费对像素 tile 计费上游的硬 502 误杀)已立案 #496 跟进。

  5. added 2 commits that reference this issue on Oct 3, 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