Skip to content

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

Open

Description

@delgadovidalbernat

Description *

  1. Started pi with billion-context (plugin native) and the model mistral/deepseek-v4-1-flash-1017 hosted by Mistral.
  2. After the conversation reached the first tool call, the follow-up request carrying the assistant tool_calls message failed. Plain-text turns work.
  3. Saw:
Mistral API error (422): {"detail":[{"type":"extra_forbidden","loc":["body","messages",5,"assistant","reasoning_content"],"msg":"Extra inputs are not permitted","input":""}]}

Expected: the request reaches Mistral without a reasoning_content field. pi's Mistral converter never emits reasoning_content (it sends reasoning as thinking content chunks), and input: "" matches the blank value injected by normalizeStrictEchoReasoning.

Cause: isStrictReasoningEcho also returns true when the body model id matches /deepseek/i (#1027 / #1028):

function isStrictReasoningEcho(session, upstreamOrigin, model) {
  if (session.metadata.strictReasoningEcho === true) return true;
  if (upstreamOrigin !== void 0 && /deepseek/i.test(upstreamOrigin)) return true;
  return typeof model === "string" && model.length > 0 && /deepseek/i.test(model);
}

That assumes every gateway serving a DeepSeek model accepts or requires reasoning_content on assistant messages. Mistral's API uses its own message schema and forbids extra fields, so reasoning_content: "" on each assistant tool-call message makes it reject the body.

Same family as #1403 / #2645.

Suggested fix: an explicit per-provider/per-model opt-out that takes precedence over the static detection, for example:

{ "providers": { "https://api.mistral.ai": { "strictReasoningEcho": false } } }

bili version *

0.1.209

Usage mode *

Plugin native (bili plugin install …, runs inside the host process)

Client and version *

pi 1.4.0

Operating system *

NixOS on WSL2

Upstream and model *

api.mistral.ai · deepseek-v4-1-flash-1017 · 1M (1048576)

Compression-related config (optional)

(default config, no overrides)

Log excerpt (strongly recommended)

From ~/.local/state/billion-context/bili.log. The model is deepseek-v4-1-flash-1017 until 17:40:16, when the same session switches to mistral-large-4 with the same history there is no strict-echo injection and the request succeeds.


17:39:55.748 [info] strict-echo: injected blank reasoning_content on 1 assistant tool-call message(s) (#762)
17:39:55.750 [info] forward POST → https://api.mistral.ai/v1/chat/completions
17:39:56.036 [warn] ← upstream 422: {"detail":[{"type":"extra_forbidden","loc":["body","messages",5,"assistant","reasoning_content"],"msg":"Extra inputs are not permitted","input":""}]}
17:40:09.444 [info] strict-echo: injected blank reasoning_content on 1 assistant tool-call message(s) (#762)
17:40:09.446 [info] forward POST → https://api.mistral.ai/v1/chat/completions
17:40:10.260 [warn] ← upstream 422: {"detail":[{"type":"extra_forbidden","loc":["body","messages",5,"assistant","reasoning_content"],"msg":"Extra inputs are not permitted","input":""}]}
17:40:16.559 [info] [window-change] deepseek-v4-1-flash-1017/plugin/1048576 -> mistral-large-4/plugin/1048576
17:40:16.564 [info] request: 9 msgs, inbound=60.0KiB, outbound=71.4KiB, local=11ms
17:40:16.565 [info] forward POST → https://api.mistral.ai/v1/chat/completions

Activity

  1. changed the title [-][Bug][pi] strict-echo model-id detection injects reasoning_content on Mistral API -> 422 extra_forbidden after the first tool call[/-] [+]strict-echo injects reasoning_content on Mistral API for DeepSeek models -> 422 extra_forbidden[/+] on Oct 30, 2026
  2. wiechapeter commented on Nov 4, 2026

    @wiechapeter

    Second report, different stack, same cause — confirms it isn't NixOS- or pi-version-specific.

    bili 0.1.212 / pi 1.5.0 / Ubuntu 24.04, model deepseek-v4-1-flash-1017 on api.mistral.ai: every tool round 422s with extra_forbidden on ["body","messages",N,"assistant","reasoning_content"], input:"". bili's log shows strict-echo: injected blank reasoning_content … (#762) immediately before the forward. Confirmed in 0.1.212's dist: isStrictReasoningEcho's model-id /deepseek/i clause fires with upstreamOrigin=api.mistral.ai, and both self-heal learners still key on status===400, so the 422 never teaches the session (no strictReasoningEchoVeto code in 0.1.212 — before #5280).
    Direct curl proof the field alone is the trigger: same body with reasoning_content:"" → 422, without → 200.

    We built #5280 on top of the v0.1.212 tag and it resolves it here: first request 422s, the veto is learned, requests 2 and 3 in the same session return 200 with the correct completion. The PR's own test file passes 5/5. Two nits on the PR: mergeable: false against current master, and its Closes #5260 points at the first-byte-stall issue, not this one.

    Interim workaround: a 15-line pi extension setting x-bili-plugin-bypass: 1 on before_provider_headers for the matched model (raw passthrough, verified 422→200 through the proxy). It costs compression on those requests, which is why we'd prefer #5280 merged.

    +1 on that PR.

  3. ranxianglei commented on Nov 4, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    感谢第二份独立报告——不同栈(Ubuntu 24.04 / bili 0.1.212 / pi 1.5.0)+ 直接 curl 对照,进一步钉死了「字段本身即触发点」的诊断。你指出的两个 nit 都已核实并处理(均独立验证过,不是照单全收):

    1. Closes 指向错误 —— 已修正。 我开 PR 时用的是内部镜像编号 5260,但上游 GitHub 的编号是 5257(GitHub 上的 #5260 是另一个 first-byte-stall 条目)。已把 PR #5280 正文的引用改为 Closes #5257。

    2. mergeable: false —— 已解决。 冲突真实存在:master 侧 #5495 在两个学习点把 demand 正则扩到了 content[].thinking(DeepSeek Anthropic-wire 拼写),与本 PR 在同一代码块重构了 veto 分支。已把 master 合入分支并按「意图并集」解冲突——veto 分支保留在前,demand 学习器采用 #5495 的扩展正则,两侧语义都不丢。分支上此前追加的 review 修复 commit(veto 清除独立成 transition,0a3d0da0f)也完整保留。

    合并后本地定向验证全绿:typecheck clean;strict-echo 三件套 67/67(含 review 新增的 demand→forbid→re-demand→accept 翻转回归);wire-contract + loop-strict-echo + loop-core + reasoning-drop 共 72/72;unused-exports gate clean。已推送(merge commit 8df9308)。

    当前状态:PR #5280 mergeable: true,mergeable_state: blocked = 仅剩 CI 未跑完(非冲突)。你的 pi 扩展临时绕行方案在 #5280 合入后就不需要了。合并由 owner 执行。

  4. Aruir commented on Nov 6, 2026

    @Aruir

    第三份报告,这次来自 DSH 桌面版:DSH 0.2.2、bili 0.1.212、Windows,api.mistral.ai 上的 deepseek-v4-1-flash-1017,原因相同。

    01:41:41.022 [info] strict-echo: injected blank reasoning_content on 1 assistant tool-call message(s) (#762)
    01:41:41.029 [info] forward POST → https://api.mistral.ai/v1/chat/completions
    01:41:42.981 [warn] ← upstream 422: {"detail":[{"type":"extra_forbidden","loc":["body","messages",8,"assistant","reasoning_content"],"msg":"Extra inputs are not permitted","input":""}]}
    

    同一个会话换成 Mistral 上的 zai-glm-5-3 后,不再注入这个字段,也没有再报 422。

    有一点请 #5280 确认:在 DSH 上,这个 422 让这一轮直接结束(PI_AI_ERROR)。如果要等第一次 422 才学会不发这个字段,DSH 用户可能要手动再发一次。我没有在 DSH 上测试 #5280。

    希望 #5280 能合入。

  5. ranxianglei commented on Nov 6, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    第三份报告(DSH 桌面版 / Windows / bili 0.1.212)与诊断完全吻合——同样的失败签名,同样的「同会话换非 deepseek 命名模型即恢复」证据。至此 pi/Linux、pi/Ubuntu、dsh/Windows 三个栈都指向同一机制。

    关于你提的 DSH 代价问题——如实回答:是的,这正是设计取舍。

    1. 第一请求会可见地失败一次。veto 是「失败学习」:只有网关自己用 extra_forbidden 拒绝过该字段,bili 才知道这个网关不接受它。这与既有的正向学习([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:上游 400 索要字段才学会开启)是同一成本模型——每个受影响会话恰好一次失败请求。
    2. 在 DSH 上如果宿主不对 422 自动重试,用户会看到那一轮以 PI_AI_ERROR 结束,需要手动重发一次。重发的请求会干净地出去并成功——因为 veto 在错误返回前就已学习并持久化(随会话元数据落盘),即使宿主在失败与重发之间重启也照样生效;此后该会话永不再注入该字段(除非出现反向的「索要字段」证据)。宿主若对 422 自动重试,则用户完全无感。
    3. 为什么不做零失败:要避免首次失败,只能二选一——(a) 硬编码「已知拒绝该字段的网关」名单:这等于回到按名字猜的老路(本 bug 的病灶就是静态名字检测),每新增一个这样的网关就是一轮新的 bug 报告;(b) 新增配置项做显式 opt-out:这是 owner 的配置面设计区,本次刻意未建;若 owner 想要这个旋钮,可单独决策。网关自己的 schema 判决是唯一可靠的信号,所以选择了学一次。

    PR #5280 当前状态:分支已 rebase 到当前 master(线性历史,head 15d86b429),冲突解法我已逐行核对保留完整(veto 分支 + #5495 扩展正则 + review 的独立清除 transition);CI 全绿(26 pass / 4 skip / 0 fail)。已具备合并条件,合并由 owner 执行。

  6. XelestiaIXIX commented on Nov 10, 2026

    @XelestiaIXIX

    补充一份 Windows / DSH 桌面版的复现信息:使用 Mistral 托管的 deepseek-v4-1-flash-1017,遇到了同样的问题。

    在 billion-context 0.1.212 上,实际日志显示:

    strict-echo: injected blank reasoning_content on 1 assistant tool-call message(s) (#762)
    forward POST → https://api.mistral.ai/v1/chat/completions
    ← upstream 422: {"detail":[{"type":"extra_forbidden","loc":["body","messages",8,"assistant","reasoning_content"],"msg":"Extra inputs are not permitted","input":""}]}
    

    更新到 0.1.213 后,检查本机安装的 dist/index.js,并提取其中的 isStrictReasoningEcho 和 normalizeStrictEchoReasoning 做了离线验证:传入 upstreamOrigin=https://api.mistral.ai、model=deepseek-v4-1-flash-1017 和一条没有 reasoning_content 的 assistant 工具调用消息,仍会开启 strict echo 并注入 reasoning_content: ""。

    说明:0.1.212 有真实请求失败日志;0.1.213 目前只做了上述代码检查和离线验证,尚未重新发起真实 API 请求。期待 #5280 修复合入并发布,谢谢!

  7. ranxianglei commented on Nov 10, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    谢谢第四份报告(Windows / DSH)。你的离线验证结果与预期完全一致,说明两点:

    1. 修复目前不在任何已发布版本里。[PR] fix: veto strict reasoning echo on gateways that reject the field as extra (#5260) #5280 的「失败学习否决」逻辑只存在于 PR 分支上;包括 0.1.213 在内的所有已发布构建都还带着原来的静态 /deepseek/i 检测与注入行为,所以按你描述的方式离线检查 dist/index.js,必然看到「仍会开启 strict echo 并注入 reasoning_content: ""」。这不是验证方法问题,是版本还没到。
    2. 四份独立复现签名一致(pi/Linux、pi/Ubuntu、DSH/Windows ×2),仅 loc 里的消息下标随历史长度不同(5、8)——诊断无分歧。

    解决路径与升级后的行为:#5280 合入 → 随下一个发版发布 → 升级到新版本后,每个受影响的会话第一次带工具调用的请求仍会失败一次(422),bili 在错误返回前就学会并持久化「该网关禁止此字段」,重发即成功,此后该会话永不再注入(宿主若对 422 自动重试则用户无感)。这是设计取舍:网关自己的 schema 判决是唯一可靠信号,不做按网关名字的硬编码名单。

    当前状态:PR #5280 开启中,head 15d86b429 的 CI 全绿(26 pass / 4 skip / 0 fail),可合并(相对最新 master 略有 behind,不阻塞合并)。合并与发版均为 owner 操作——我不会自行切版;如需催办,请在 #5280 上向 owner 提出。

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions