Skip to content

[PR] fix: veto strict reasoning echo on gateways that reject the field as extra (#5260) - #5280

Open
ranxianglei wants to merge 3 commits into
masterfrom
2026-10-31_strict-echo-veto
Open

ranxianglei wants to merge 3 commits into
masterfrom
2026-10-31_strict-echo-veto

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #5257

Problem

pi plugin-native + Mistral serving a deepseek-named model (deepseek-v4-1-flash-1017): from the first tool call onward every follow-up request 422s forever —

422 {"detail":[{"type":"extra_forbidden","loc":["body","messages",5,"assistant","reasoning_content"],"msg":"Extra inputs are not permitted","input":""}]}

The static /deepseek/i detection (#1027) makes bili inject a blank reasoning_content on assistant tool-call messages (#762). That is correct for DeepSeek's own API (which demands it) but Mistral's message schema forbids the field entirely — and nothing ever learned the gateway rejects it, so the session was stuck in a permanent inject→422 loop. The reporter's log proves it: switching the same session to mistral-large-4 (no name match) succeeds immediately.

Fix: learn-on-failure VETO (mirrors #684/#762/#2169 in the opposite direction)

  • src/strict-echo.ts: new pure predicate isReasoningEchoForbiddenReject(status, body) — true only for a 4xx whose body names the echo field (reasoning_content/reasoning_text) AND rejects it as extra/unknown (extra input(s), not permitted, unrecognized, unknown field). Demand-class wording (DeepSeek's own "must be passed back") never matches.
  • New per-session key session.metadata.strictReasoningEchoVeto, checked FIRST in isStrictReasoningEcho — it outranks both the learned flag and static detection.
  • Both non-2xx surfaces learn it: relay main path (src/server/relay.ts) and loop re-request (src/loop/core.ts). The existing 400 demand-learner became an else if and clears the veto when the upstream re-demands the field, so the two evidence classes can flip the session either way.
  • Wire-constraint ledger grew ([CI/testing] wire-contract anti-regression system: validation-parity fake upstreams + golden schema snapshots + constraint ledger + staged 4-protocol x 3-mode client smoke matrix #1304): WC-023 in tests/wire-contract-fakes.ts (Mistral chat lane forbids reasoning_content on any message), enforced by a new mistral validation-parity lane on the openai-chat fake + a WC-023 row in the ledger self-check.
  • Regression suite: tests/issue5260-strict-echo-veto.test.ts — unit truth table (exact Mistral 422 body vetoes; demand wording / no-marker / no-field / 200 / 500 never do; veto beats static and learned evidence) + main-path e2e through the mistral-lane fake: request 1 → 422 + exactly one WC-023 violation + visible injection, request 2 → 200 with a clean body and no new violation.

Behavior change (old → new)

Old: once strict-echo evidence fired (static model match or learned flag) it stayed ON for the whole session — a gateway that forbids the field rejected every tool-call turn forever.
New: one 4xx extra-forbidden rejection of the field turns strict echo OFF for that session (cost: exactly one failed request per affected session — the same cost model as the original #684 learning). A subsequent demand-class 400 clears the veto and re-enables it. No change for gateways that accept or demand the field.

Config surface

None. No new config keys / env vars / CLI flags. The reporter proposed an explicit per-provider opt-out (providers[...].strictReasoningEcho: false); that is owner-design territory per the Configuration Surface Discipline and was deliberately NOT built — automatic learning covers the case without a new knob. If an explicit opt-out is wanted, it should be decided separately on the config surface.

Tests (local, targeted)

  • typecheck clean
  • tests/issue5260-strict-echo-veto.test.ts tests/reasoning-echo.test.ts tests/reasoning-echo-400.test.ts → 66/66
  • tests/wire-contract.test.ts → 22/22 (incl. WC-023 lane probe)
  • tests/loop-strict-echo.test.ts tests/loop-strict-echo-responses.test.ts tests/loop-core.test.ts tests/reasoning-drop.test.ts → 49/49

No kernel changes.

@ranxianglei ranxianglei changed the title fix: veto strict reasoning echo on gateways that reject the field as extra (#5260) [PR] fix: veto strict reasoning echo on gateways that reject the field as extra (#5260) Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-31_strict-echo-veto (15d86b4)

Latest build — install this exact version

npm install -g billion-context@0.0.8132-pr.5280.68f9dc8

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Recent builds (commit → version)

commit version build
15d86b4 billion-context@0.0.8132-pr.5280.68f9dc8 build
8df9308 billion-context@0.0.8130-pr.5280.a87c6c8 build
0a3d0da billion-context@0.0.7311-pr.5280.49b5e46 build
1abc8bd billion-context@0.0.7267-pr.5280.3fcdb2a build
46838f8 billion-context@0.0.7261-pr.5280.b75ed63 build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr5280.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

First push tripped the unused-exports gate: mistralErrorEnvelope in tests/wire-contract-fakes.ts was exported but only used internally by startFakeUpstream's envelope selection. Fixed in 1abc8bd25 — dropped the export (INTERNAL_ONLY per the gate's own hint), no behavior change. Gate re-run locally clean, tests/wire-contract.test.ts still 22/22.

@ranxianglei ranxianglei added bug Something isn't working p1 pi scope/wiring labels Oct 31, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

状态更新:新 head 0a3d0da0f 的 CI 目前 0 failed,剩 19 条车道在排队等 runner(分片 test gate 受舰队并发限制),非失败。本地定向证据已在上一条结论里给出(8 套件 138/138 + typecheck clean + 新增翻转测试在修复前代码上实测失败)。绿了在此补最终确认。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

收工摘要(本 PR 的 review 工作已全部完成,细节见上方结构化八问结论):

  1. 审核:独立重审了 PR 全部 6 个文件——谓词设计、veto 优先级、两个学习点、WC-023 lane-scoped 账本、回归测试有效性(diff 逐行 + 既有行为逐项清点,无无意回归)。
  2. 发现并直接修复一处缺陷(commit 0a3d0da0f,已推到 PR 分支):初版把 veto 清除嵌套在 learned-flag 翻转守卫里,「先 learned ON、后被 veto」的会话在上游再次 demand 时 veto 永不清除 → 反向永久 400 循环([PR] fix: first-byte stall warn per (origin,session) with cooldown instead of once-per-origin-per-process (#5164) #5260 stuck loop 的镜像)。修法:veto 清除改为独立转移(relay/loop 两处对称,各带自己的 warn log + markDirty);新增四阶段翻转回归测试(demand→forbid→re-demand→accept),实测在修复前代码上失败、修复后通过。
  3. 本地证据:8 个相关套件 138/138 + typecheck clean(全量裁决以远端 CI 为准)。
  4. 唯一待办:新 head 0a3d0da0f 的 CI 目前 0 failed、19 条车道排队等 runner(舰队并发限制,非失败)。绿了在此补最终确认;若红则按 CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 协调流程处置。

判定不变:可合(待 CI 绿确认)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

路由澄清:本会话被派单为 #5288,但实际要处理的 issue「小窗口上下文 token 截断」的内容与讨论都在 #5289(本条 #5288 是另一个无关的 strict-echo veto PR,我不在这里展开)。完整排查与回复见 #5289。此处仅补一句结论,避免派单跟踪以为没产出。

做了什么:独立把代码逐行核了一遍,坐实了截断根因——bili 拦截 DSH 原生压缩请求时记下一个「只增不减、永不衰减」的输入下限(dshCompactionRefusedBytes,src/server/handle.ts:1996-1997),它在「本地应答」和「拒绝」两条路径都会写入,把出站 max_tokens 在小窗口(llama.cpp 128K)下钳到 ~33K,长回复即被截断;统计图显示的 49K 与 DSH 本地 93K→160K 是两个不同口径,各对各的真话。

结果:已在 #5289 给出①代码级机制确认(含上轮没点透的「两条路径都钉死下限」关键点)、②用户可立即执行的客户端侧缓解(让 DSH 别发原生压缩请求,#2474 覆盖)、③根治修法设计。根治动的是 #2490 的保护语义(行为变更),按纪律等用户日志确认 + owner 拍板 A/B 选项后再实现,本轮未改代码。当前状态:等用户在 #5289 回日志与接入方式。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

状态更新(本条 #5288 只是派单跟踪指针,实际排查与讨论全在 #5289,此处不重复内容):截断机制已在代码层面坐实并复现算术,当前卡在等 owner 在 #5289 拍板修法选项(A 应答路径下限回落=推荐 / B 双路径跟随实测 / C 收紧门控)。属 #2490 行为变更,需你点头我才动代码。详见 #5289 最新一条。本轮无代码改动。

ci-check added 3 commits November 4, 2026 18:34
Review finding: both learn sites nested the veto clear inside the learned-flag flip guard (relay demand branch / loop UpstreamHttpError else-if). A session holding BOTH evidence classes — learned ON from an earlier demand 400, later vetoed by an extra-forbidden 4xx — could never clear its veto: re-demand evidence arriving while the flag was already true did nothing, so every tool-call turn 400'd forever in the opposite direction (the mirror image of #5260's stuck loop). The veto clear is now an independent transition in src/server/relay.ts and src/loop/core.ts, with its own warn log and markDirty, so the two evidence classes keep flipping the session either way. Regression test drives the full flip through the real relay (demand → forbid → re-demand → accept) and fails on the pre-fix code.
@ranxianglei
ranxianglei force-pushed the 2026-10-31_strict-echo-veto branch from 8df9308 to 15d86b4 Compare November 4, 2026 10:50
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 既有 PR(#5280)的接冲突延续处理,非新任务——未开任何新 issue/PR。

本轮做完的事

  1. 把 PR 分支 2026-10-31_strict-echo-veto rebase 到最新 master(23a826d37,即 [PR] Fix #5618 usage wall: remote-URL / file-ref media becomes foldable, its re-access handles ride the summary #5629 合入后)。
  2. 冲突共两处、同一种形状(src/loop/core.ts ~L1720、src/server/relay.ts ~L1590):master 侧 Anthropic-wire upstream without thinking signatures (DeepSeek /anthropic): compress re-request drops reasoning from the tool_use replay → 400 "content[].thinking must be passed back" #5495 给「learn strict echo」的正则加了第三种字段拼写 content[].thinking,本 PR 侧 [PR] fix: first-byte stall warn per (origin,session) with cooldown instead of once-per-origin-per-process (#5164) #5260 把同一段包进了 isReasoningEchoForbiddenReject 否决分支。解法取双方意图的并集:保留 Anthropic-wire upstream without thinking signatures (DeepSeek /anthropic): compress re-request drops reasoning from the tool_use replay → 400 "content[].thinking must be passed back" #5495 注释+扩展正则,同时保留 PR 的否决结构(含 commit 3 的 review 修复——「清除否决是独立转移」)。
  3. rebase 后本地定向验证:typecheck 干净;tests/issue5260-strict-echo-veto.test.ts 5/5 过、wire-contract 28/28 过;diff 范围仍只有原 PR 的 6 个文件。
  4. 推送时发现一个并行 worker 已把 master merge 进了分支(8df9308a2,语义相同的解法,与我仅差一行换行)。按仓库惯例(线性历史优先)force-push 了我这边已验证的 rebase 版本,当前 head 15d86b429。
  5. PR 状态:dirty → blocked(冲突已消,现只等 CI 绿)。新 head 上 CI 已在跑:static-gates / mysql-contract / config-surface-guard 已绿,test 分片进行中。

八问

  1. 是什么问题:PR 分支落后 master,合并状态 dirty,两个文件里存在文本冲突。
  2. 什么原因导致的:PR 拉出后 master 合入了 Anthropic-wire upstream without thinking signatures (DeepSeek /anthropic): compress re-request drops reasoning from the tool_use replay → 400 "content[].thinking must be passed back" #5495(DeepSeek Anthropic-wire 报错字段正则加 content[].thinking 拼写),与本 PR 对同几行的 [PR] fix: first-byte stall warn per (origin,session) with cooldown instead of once-per-origin-per-process (#5164) #5260 否决改造相撞。
  3. 影响范围与严重程度:只影响本 PR 自身的可合并性;无用户可见行为变化——解法是双方语义并集,谁的行为都没丢。
  4. 怎么修复的:rebase + 两处冲突按意图并集手工解决(扩展正则与否决结构都保留),force-push。
  5. 是否有回归问题:逐项核过触碰路径的既有行为——Anthropic-wire upstream without thinking signatures (DeepSeek /anthropic): compress re-request drops reasoning from the tool_use replay → 400 "content[].thinking must be passed back" #5495 第三拼写匹配保留、[bug] compress 保护区过滤在完整性调整之后挖洞,导致 assistant tool-call 消息缺 reasoning_content(DeepSeek 思考模式 400) #684/[Bug] Responses 线路 strict-echo (#1479) 注入的空白 reasoning 被严格后端拒绝(DeepSeek: reasoning_text must be passed back) #2169 learn 逻辑保留、[PR] fix: first-byte stall warn per (origin,session) with cooldown instead of once-per-origin-per-process (#5164) #5260 否决/清除转移保留(含 clear 独立转移的修复);无语义漂移、无 fail-fast 变吞错、无阈值/时序/输出格式漂移。本地 typecheck + 定向测试绿;全量回归以远程 CI 为准(§7.2)。
  6. 是否破坏缓存:不涉及——未触碰缓存、重试游标、去重键逻辑,只动了 loop/relay 里的 4xx 证据判定分支。
  7. 是否改了设计方向:不涉及,纯机械接冲突。
  8. 是否引入新的配置:不涉及。

收工判定:冲突已解决并推送,merge state 干净(仅剩 CI 门禁在跑);若 CI 出现红项,我作为本 PR 的 CI 负责 bot 会在本线程跟进(auto-fix lane,attempt 1/3)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

Verdict: APPROVE — clean fix, real regression tests, no kernel changes, CI green on head 15d86b429.

结构化八问(中文结论)

  1. 是什么问题:pi plugin-native 用 Mistral 托管 deepseek-named 模型(deepseek-v4-1-flash-1017)时,从第一个 tool call 起每个后续请求都 422 extra_forbidden——Mistral schema 整体拒绝 bili 注入的空 reasoning_content,会话永久卡在 inject→422 循环(报告者日志:同一会话切到 mistral-large-4 立即恢复)。
  2. 什么原因导致的:资格判定只学「需求类」证据(DeepSeek "must be passed back" → 开 strict-echo),对「禁止类」证据(网关把注入字段当 extra 拒掉)没有反向学习;bug: strict-echo is host-only — DeepSeek models behind a non-deepseek gateway re-pay the reasoning_content 400 on every new session #1027 静态 /deepseek/i name-match 一旦命中就整会话锁死 ON。因果分层:name-match 触发注入(bug: strict-echo is host-only — DeepSeek models behind a non-deepseek gateway re-pay the reasoning_content 400 on every new session #1027/DeepSeek thinking mode: residual "reasoning_content must be passed back" 400 on the first post-fold request (0.1.109 / acp-kernel 0.0.69) — the forwarded body is not persisted #762) → Mistral schema 拒字段 → 无否决机制 → 永久循环。
  3. 影响范围与严重程度:任意「非 DeepSeek 官方网关托管 deepseek-named 模型」的插件原生会话,从首个 tool call 起完全不可用(每轮 422)。该配置下 p1 级(会话不可用)。修复后成本 = 每个受影响会话恰好一次失败请求(与 [bug] compress 保护区过滤在完整性调整之后挖洞,导致 assistant tool-call 消息缺 reasoning_content(DeepSeek 思考模式 400) #684 原学习同成本模型)。
  4. 怎么修复的:learn-on-failure VETO,镜像 [bug] compress 保护区过滤在完整性调整之后挖洞,导致 assistant tool-call 消息缺 reasoning_content(DeepSeek 思考模式 400) #684/DeepSeek thinking mode: residual "reasoning_content must be passed back" 400 on the first post-fold request (0.1.109 / acp-kernel 0.0.69) — the forwarded body is not persisted #762 正向学习的反方向。新纯谓词 isReasoningEchoForbiddenReject(status,body)——仅当 4xx 且 body 同时点名回显字段(reasoning_content/text)且带 extra/not permitted/unrecognized/unknown field 标记时为真(DeepSeek 需求措辞不含这些标记,不误判)。新会话键 session.metadata.strictReasoningEchoVeto 在 isStrictReasoningEcho 最先判定、压过 learned flag 与静态检测。两个非 2xx 面都学它(relay forwardCaptured + loop core.ts);原 400 需求学习降为 else-if,并在反向证据到达时清 veto,两类证据可双向翻转。
  5. 是否有回归问题:逐项过既有行为——①纯需求类 400(无 veto 状态)新旧逐字节等价(flag 只置一次);②isStrictReasoningEcho 对无 veto 会话逐字节不变;③持久化已确认:loop 共享 relay 的活 session,relay 流式/非流式 finally 都 markDirty(prepared.session)(客户端取消 = 无主 AbortError ×260/天 + 已计费 usage 丢弃(lastInputTokens 冻结)+ 透传路径漏 10min timer(无 finally) #411),与既有 flag 同机制,veto 跨回合存活;④附带效应:veto 使谓词=false,同时重新启用 Protected-message exemption inflates the incompressible context floor: per-compression reasoning is never reclaimable (~83.5% of measured residual) #651 reasoning-drop 与 fold-point thinking-clear——对「禁字段」网关方向正确(Mistral 不需要 round-trip),signed-thinking 独立门仍护签名块;⑤顺带纠正一处潜在误分类:带 extra 标记的 400(如 "Unrecognized request argument: reasoning_content")旧代码会误学 ON,现正确走 veto。高危模式(语义漂移/fail-fast 变吞错/阈值时序输出格式漂移)逐项过:无。
  6. 是否破坏缓存:无实质影响。veto 改出站 body 字节(不再带 reasoning_content)是修复必需且会话此前已坏;veto 粘性后 body 又稳定,不动前缀缓存命中策略本身。
  7. 是否改了设计方向:无架构转向。复用既有 per-session metadata 学习机制(正向学习镜像),符合 learn-on-failure 范式。未发明新 config 面——报告者建议的 providers[...].strictReasoningEcho:false 属 owner-design territory,按 Configuration Surface Discipline 明确不做并披露。
  8. 是否引入了新的配置:无新 env/config/CLI flag。(opt-out 提案刻意推迟到 config 面单独决策。)

测试有效性(真测试、有牙):①isReasoningEchoForbiddenReject 双向 truth table(精确 Mistral 422 / OpenAI "Unrecognized request argument" / "Unknown field" 命中;DeepSeek 需求措辞 / 仅字段无标记 / 仅标记无字段 / 200 / 500 全不命中);②precedence(veto 压 static + learned,含 reporter 精确 case);③主路径 e2e 过 mistral-lane validation-parity fake(req1→422 + 一条 WC-023 + 可见注入;req2→200 clean、无新 violation、body 无 reasoning_content、全程仅一次注入);④state-machine flip test 钉住首版 draft 会漏的子 case(learned ON 后再 veto,re-demand 到达时 flag 已 true 仍须清 veto)。回归时抓得住。

Wire-contract ledger (#1304):WC-023 永久入账(summary + provenance 齐),lane-scoped validator validateMistralChatBody 正确放在 mistral lane 而非共享 openai-chat VALIDATOR(OpenAI 自身接受该字段),ledger self-check 有 WC-023 probe row。✓

Non-blocking observations(不拦合、未改)

结论:可合。 CI 绿(head 15d86b429:ubuntu/windows × node22/24 全部 test lanes success,static-gates success,red-signal skipped;mergeable_state=blocked 即等待审批、非 CI 失败)。


说明:本账号即 PR 作者(agent 以 ranxianglei 身份推分支),GitHub 禁止对自有 PR 提交正式 approve,故本审核结论以评论形式提交;正式合并由 owner 手动执行。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

审核完成——结论 APPROVE(可合)。上面那条就是本 lane 提交的完整八问审核(GitHub 禁止对自有 PR 正式 approve,故以评论形式落在 PR #5280;正式合并由 owner 手动执行)。

一句话状态:修复正确、测试有牙、无 kernel 改动、无新配置面、无回归;CI 绿(head 15d86b429)。两条非阻塞观察已写在评论里(标题括注 #5260 指向无关 PR,正文 Closes #5257 才对;谓词未覆盖 #5495 的 Anthropic-wire content[].thinking 拼写)——均不拦合、本次未改。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working p1 pi scope/wiring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

strict-echo injects reasoning_content on Mistral API for DeepSeek models -> 422 extra_forbidden

1 participant