Skip to content

[Bug] fold reconstruction can emit two adjacent output_config effort directives → deterministic 400 on Anthropic lane (pi thinkingLevel max + OpenRouter) #5688

Open

Description

@john08281

Summary

After a fold (compress) is applied to an Anthropic-lane session, the reconstructed outbound history can end up containing two adjacent {role:"system", content:[], output_config:{effort:...}} directives, which the upstream rejects with HTTP 400:

400 {"type":"error","error":{"type":"invalid_request_error","message":"Invalid Anthropic Messages API request"},
"metadata":{"raw":"[{\"code\":\"custom\",\"path\":[\"messages\",2,\"output_config\"],
\"message\":\"Two adjacent output_config directives are not allowed\"}]"}}

The client is pi 1.5.0 (anthropic-messages API over OpenRouter, model anthropic/claude-opus-5.5, thinking level max). pi emits one output_config directive before every assistant message that carries an effort level, plus one trailing directive for the active level (OpenRouter "per-turn effort" beta). Those directive messages are part of the wire history the proxy assigns refs to.

Evidence chain (all reproduced today)

# Test Result
1 Minimal request with two adjacent output_config system directives → OpenRouter 400, exact same error
2 Same shape with a single directive / normal spacing 200 OK
3 Full replay of the client's 197-message history (with all directives), direct to OpenRouter, no proxy 200 OK
4 Same history through the proxy after a fold 400 (the bug)

Test 3 is the decisive one: the client's own sequence is healthy; the adjacency only appears after the proxy's fold reconstruction ([Compressed m00001–m00053, m00054–m00120, m00121–m00149, m00150–m00170 → 1 block(s), ~32157 tokens saved]).

Session dies permanently afterwards: every retry re-sends the same history, the fold state is persistent, the 400 reproduces deterministically (observed twice 60s apart with only "继续" in between).

Reproduction sketch

  1. pi + opus-5.5 via OpenRouter (supportsMidConvoEffort: true model), thinking level max, routed through the proxy.
  2. Work normally until a fold fires that covers a span containing several effort directives.
  3. The next request after the fold returns the 400 above. Retries keep failing.

(Exact upstream stack: OpenRouter /api/v1/messages — the same surface documented in their per-turn-effort beta, where "Two adjacent output_config directives are not allowed" is an upstream validation rule.)

What I checked in the source

  • output_config appears nowhere in the proxy's own transform path (only read by cache-ledger keying / output-steering, both inert here) — the directives pass through untouched.
  • So the adjacency is produced by sequence reshaping during fold reconstruction: dropping/merging messages between two directives (reasoning hygiene, CCR excerpting, range trimming, or empty-message pruning) can leave two directive messages next to each other.

Ask

Fold reconstruction (or the outbound invariant judge, which already scans every request) should treat adjacent output_config directives as a repairable defect: merge adjacent directives (keep the latter) before egress, or at minimum warn when the outbound contains them.

Happy to provide the exact session/index.db dumps on request — the histories contain private content so they are not attached here.

  • proxy version: v0.1.212
  • client: pi 1.5.0, anthropic-messages lane, provider openrouter, model anthropic/claude-opus-5.5, thinkingLevel max

Activity

  1. ranxianglei commented on Nov 4, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重:不重复。 多组关键词("output_config"、"adjacent directives"、"effort 400"、"adjacent system directives merge egress repair")检索 open+closed issue/PR,无既有条目记录本缺陷。存在同家族近亲:#4570 / #5437(mid-conversation system 位置违规 400,修复在 PR #4715 / #5441 推进中)——但那是 Anthropic 的位置规则("role 'system' must precede/follow …"),且那两条规则里 directive-only 形态(content: [] + output_config)被明确豁免("accepted at any position"),现有 heal 对它完全不碰;本条是 OpenRouter per-turn-effort 的另一条校验规则("Two adjacent output_config directives are not allowed"),producer 也不同:fold 重建删掉两条 directive 之间的消息后把两条豁免 directive 顶成相邻,而非追加/孤儿化的位置漂移。同家族、不同症状、不同机制 → 单独立项成立,后续回复里会交叉链接两个家族。

    标签:bug / pi / scope/wiring / p1 —— 缺陷类;pi 车道(OpenRouter anthropic-messages);硬失败在出站管线;p1 依据:fold 状态持久、每次重试原样重发同一历史、确定性 400 死循环、会话在人工干预前完全不可用(与 #4570 里 pi 数据点同一升档口径)。

    审核与计划:你的证据链是完整的——尤其 test 3(客户端 197 条历史直连 OpenRouter 200)钉死了「客户端自身序列健康、相邻性由 proxy fold 重建产生」,我无需再向你要 dump 来定性。源码侧我先核了你说的两点(output_config 在 src/ 只出现在 cache-ledger 键控与 output-steering 读取,transform 路径确实不碰 directive)。接下来按序做:① 在 fold 重建路径上定位具体 producer(哪一层把两条 directive 之间的消息吃掉);② 最小 hermetic 复现测试;③ 按你的 ask 实现出站修复——相邻 output_config directive 合并(保留后者),落点选在最终出站数组的既有校验点旁(与 #4715/#5441 的最终数组 heal 同层,避免 kernel 门禁阻塞止血);④ PR 引用本 issue。进展在本线程汇报。

  2. john08281 commented on Nov 4, 2026

    @john08281
    ContributorAuthor

    PR #5690 already implements the egress repair (③ in your plan): dedupeAdjacentOutputConfig collapses adjacent directive runs to the latter, wired in prepare-anthropic.ts right where the final outbound array is assembled (next to warnAnthropicThinkingPairs, same layer as the #4715/#5441 heal). 7 unit tests included (node:test). Happy to adjust the landing point or detection shape if the producer analysis (①/②) turns up a better seam — the helper itself is placement-agnostic.

  3. ranxianglei commented on Nov 4, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    收到 PR #5690,已独立核验并做完 producer 分析(①/②)。结论:方向正确、实现干净,已补上缺失的接线级回归测试,可交 owner 合并。 下面是结构化审核。

    独立核验(不照抄 PR 描述)

    • PR 存在且 base = c9ba48566 == 当前 origin/master head(fresh base,无需 rebase)。
    • 本地实跑:PR 的 7 个单测全绿;npm run typecheck 干净;相邻面 tests/anthropic-lane-nudge.test.ts + tests/issue1470-anthropic-nudge.test.ts 5/5 绿。
    • 审核期间作者又推了两个 commit(8cab8fe45 helper 抽到独立模块 src/dedupe-adjacent-output-config.ts,95ddfa482 把 dedupe 扩到 nudge-decide side body 和 loop round-2 rebuild)——我逐一读过 diff,质量没问题;我原本准备自己补的 sideBody 修复因此作废(作者的覆盖更全,还多盖了 round-2)。

    结构化八问

    1. 是什么问题:fold(压缩重建)之后,Anthropic 线上会连续出现两条 {role:"system", content:[], output_config:{effort:...}}(effort 指令消息),OpenRouter 按上游校验规则硬拒 400("Two adjacent output_config directives are not allowed"),会话此后每次重试都重发同一段历史,永久死循环。
    2. 什么原因导致的:不是客户端错——你测试 3 已钉死(客户端原始历史直连 200)。根因在 bili 的出站重建:pi 在每个带 effort 的 assistant 消息前各放一条指令、末尾再放一条,指令之间原本隔着 assistant 消息;fold 之后,夹在两条存活指令之间的内容被移除(folded span 的 carrier 隐藏是设计行为:hideConsumedCompressCalls 对 inactive 块对应的 compress call/result 对无条件隐藏,kernel/src/hide-consumed.ts:333;summary 锚剥离在 src/server.ts:2756),两条指令就贴到一起了。最小自然触发链不需要 tier merge:一次 fold 覆盖"两条存活指令之间的 span"即产生邻接(你日志里 4 ranges→1 block 是它的批量形态)。所以修复点只能在 egress 层——产出机制本身是缓存/上下文卫生,不能从源头"修掉"。这正是你 Ask 里的方案。
    3. 影响范围与严重程度:pi(或任何发 per-turn effort 指令的宿主)+ OpenRouter anthropic-messages 线 + thinkingLevel 开启;一旦触发该会话完全不可用(确定性 400,重试无效,只能人工清会话)→ p1。其他 provider/车道不受影响(该校验规则是 OpenRouter 侧的)。
    4. 怎么修复的:egress 折叠——在每个 anthropic-wire 出站面的最终组装点,把连续指令序列收敛为最后一条(later effort wins,与末位 active 指令语义一致)。检测刻意收窄:仅"空 content 的 system + output_config 对象",普通 system 消息(有文本)绝不触碰;无邻接时原样返回输入数组引用(字节稳定快路径)。三个出站面全覆盖:steady prepare egress(prepare-anthropic.ts:606)、nudge-decide side body(:535)、loop round-2 rebuild(adapter-anthropic.ts:314,已验证是该文件唯一 egress 点)。
    5. 是否有回归问题:审过的既有行为逐项过——干净路径字节不变(reference-identity 快路径,单测第 1 例 + 新接线测试负向例双钉);warnAnthropicThinkingPairs 不受影响(删的是 system 指令,不碰 assistant thinking 对);kernel transform 失败的 catch 路径(转发原始请求)因 dedupe 点在最终组装处而同样受益;nudge 三个分支追加的都是 user 消息,不可能引入指令;无意回归:未发现。
    6. 是否破坏缓存:不额外破坏。fold 轮本身已使前缀缓存失效;指令折叠不改变任何其他字节,干净请求零字节变化。
    7. 是否改了设计方向:没有。纯 host-side 出站修复层,不碰 kernel/,不动 carrier 隐藏机制本身。
    8. 是否引入新配置:没有。无新 env/config/flag。

    行为变更披露(旧值→新值):fold 后出现邻接指令时——旧行为:确定性 400、会话永久死亡;新行为:出站收敛为最新 effort 的单条指令,请求 200 通过。这就是修复目标本身,按要求显式披露。

    本次审核新增(已推到 PR 分支)

    状态与判定

    • 分支当前 head:2b3ef9f77(你两个新 commit + 我的测试 commit);CI 在新 head 上待跑(镜像缓存暂无数据,跑完我会再看一眼)。
    • Loop guard:本 issue 只对应这一个 PR,我没有另开 PR。
    • 判定:代码与测试达标、diff 干净、base 新鲜,可交 owner 合并(合并权在人,我不代合)。
  4. ranxianglei commented on Nov 4, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    🔶 需要决策

    背景一句话:PR #5690 的三个 head(3403429df / 8cab8fe45+95ddfa482 / 2b3ef9f77)全部零 check-run——外部 fork 的 PR 在 GitHub 上需要 owner 在 PR 页面点一次 "Approve and run workflows",CI 才会启动;不点,分支保护的 required checks 永远满足不了(mergeable_state=blocked),PR 无法合并。

    待决问题:是否批准该 fork 在本仓库运行 Actions?

    选项:

    推荐 + 不决定的默认行为:选 A。不点的话 PR 维持 blocked、停在原地等;本地验证证据已在上一条回复给全(9/9 定向测试绿 ×2 连跑 + typecheck 干净 + anthropic nudge 相邻套件 5/5 + 负控制证明断言有牙),但本地结果不能替代 CI 的全量裁决。

    标签:已加 needs-decision(CI 被 fork 审批门禁卡住,需 owner 人工动作)。

  5. added 2 commits that reference this issue on Nov 5, 2026
  6. added a commit that references this issue on Nov 5, 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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions