Repository navigation
Description
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
- pi + opus-5.5 via OpenRouter (
supportsMidConvoEffort: truemodel), thinking levelmax, routed through the proxy. - Work normally until a fold fires that covers a span containing several effort directives.
- 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_configappears 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
ranxianglei commentedon Nov 4, 2026
(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_configdirective 合并(保留后者),落点选在最终出站数组的既有校验点旁(与 #4715/#5441 的最终数组 heal 同层,避免 kernel 门禁阻塞止血);④ PR 引用本 issue。进展在本线程汇报。john08281 commentedon Nov 4, 2026
PR #5690 already implements the egress repair (③ in your plan):
dedupeAdjacentOutputConfigcollapses adjacent directive runs to the latter, wired inprepare-anthropic.tsright where the final outbound array is assembled (next towarnAnthropicThinkingPairs, 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.refactor: extract dedupeAdjacentOutputConfig to its own module (ranxi…
fix: apply adjacent output_config dedupe to nudge-decide side body an…
test: pin the ranxianglei#5688 egress dedupe through the full prepare…
ranxianglei commentedon Nov 4, 2026
(ai: vllm-qwen/qwen3.8-27b)
收到 PR #5690,已独立核验并做完 producer 分析(①/②)。结论:方向正确、实现干净,已补上缺失的接线级回归测试,可交 owner 合并。 下面是结构化审核。
独立核验(不照抄 PR 描述)
c9ba48566== 当前 origin/master head(fresh base,无需 rebase)。npm run typecheck干净;相邻面tests/anthropic-lane-nudge.test.ts+tests/issue1470-anthropic-nudge.test.ts5/5 绿。8cab8fe45helper 抽到独立模块src/dedupe-adjacent-output-config.ts,95ddfa482把 dedupe 扩到 nudge-decide side body 和 loop round-2 rebuild)——我逐一读过 diff,质量没问题;我原本准备自己补的 sideBody 修复因此作废(作者的覆盖更全,还多盖了 round-2)。结构化八问
{role:"system", content:[], output_config:{effort:...}}(effort 指令消息),OpenRouter 按上游校验规则硬拒 400("Two adjacent output_config directives are not allowed"),会话此后每次重试都重发同一段历史,永久死循环。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 里的方案。prepare-anthropic.ts:606)、nudge-decide side body(:535)、loop round-2 rebuild(adapter-anthropic.ts:314,已验证是该文件唯一 egress 点)。warnAnthropicThinkingPairs不受影响(删的是 system 指令,不碰 assistant thinking 对);kernel transform 失败的 catch 路径(转发原始请求)因 dedupe 点在最终组装处而同样受益;nudge 三个分支追加的都是 user 消息,不可能引入指令;无意回归:未发现。kernel/,不动 carrier 隐藏机制本身。行为变更披露(旧值→新值):fold 后出现邻接指令时——旧行为:确定性 400、会话永久死亡;新行为:出站收敛为最新 effort 的单条指令,请求 200 通过。这就是修复目标本身,按要求显式披露。
本次审核新增(已推到 PR 分支)
2b3ef9f77:tests/issue5688-egress-wiring.test.ts—— 原 PR 只有 helper 级单测,没有任何测试钉住接线(把调用点删掉 CI 照样绿)。新测试用 hermetic A/B rig(mock upstream 逐字执行 OpenRouter 的邻接规则,违反即回同款 400)驱动真实/bili/请求:正向断言折叠后的单条 keep-latter 指令到达上游且 200;负向(Test Blind Spot Retrospective: Reinforcing Negative Assertion Discipline, Tightening Fake Upstream Strictness, Thin Inst | 测试盲区复盘:补反向断言纪律、假上游严格化、薄安装冒烟、形态矩阵与生命周期覆盖 #3617 双向纪律)断言健康 pi 形状逐条通过、零邻接、effort 序列不变。负控制已做:临时禁用 seam → 正向红、负向保持绿,证明断言有牙。本地 9/9 绿(连跑两遍)+ typecheck 干净。状态与判定
2b3ef9f77(你两个新 commit + 我的测试 commit);CI 在新 head 上待跑(镜像缓存暂无数据,跑完我会再看一眼)。ranxianglei commentedon Nov 4, 2026
(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?
选项:
2b3ef9f77上跑完,绿了即可合并。推荐 + 不决定的默认行为:选 A。不点的话 PR 维持 blocked、停在原地等;本地验证证据已在上一条回复给全(9/9 定向测试绿 ×2 连跑 + typecheck 干净 + anthropic nudge 相邻套件 5/5 + 负控制证明断言有牙),但本地结果不能替代 CI 的全量裁决。
标签:已加
needs-decision(CI 被 fork 审批门禁卡住,需 owner 人工动作)。fix: collapse adjacent output_config effort directives after fold rec…
Merge pull request #5690 from john08281/fix/5688-adjacent-output-config
Merge upstream/master (9a2d51d): adopt ranxianglei#5718 sidecar-resid…
Merge origin/master into 2026-11-01_outbound-prefix-lcp-triage — reso…