Repository navigation
[Bug] #762's strict-echo repair is chat-completions-only: Responses-wire requests still 400 with reasoning_content_missing after a compress fold #1479
Description
Description *
Symptom
Same rejection class as #762, but on the Responses wire (wire_api = "responses"), where the #762 repair does not exist.
- 7 requests rejected with
extError.code = "reasoning_content_missing"/code = 11155— "the reasoning content from the previous turn must be passed back in thinking mode". - Every one of them is the round-2 re-request that immediately follows a
compressfold. strict-echo: injectedfired 0 times in the whole 7,707-line log, whilereasoning-pair-violatedfired 798 times.
Why the #762 fix cannot fire here
master @ 54c433a1 (v0.1.159):
src/strict-echo.ts:35normalizeStrictEchoReasoning(messages, enabled, log, sessionId)injectsreasoning_content: ""into assistant tool-call messages lacking the field.src/strict-echo.ts:61-67normalizeStrictEchoBodyguards withif (!enabled || !Array.isArray(body.messages)) return body;— a Responses body carriesinput[], notmessages[], so this is a guaranteed no-op on this wire.src/server.ts:3146is the only call site, insideprepareOpenai(chat-completions).- The Responses path (
prepareResponses,src/server.ts:3399) does the opposite of repairing:src/server.ts:3660builds{ ...parsed, input: rebuiltInput, tools: toolsOut }andsrc/server.ts:3661merely callswarnResponsesReasoningPairs(...)— alog("warn", ...)-only canary (src/server.ts:2509-2530). - The fold repair on this path inserts a user separator, not a reasoning item:
src/server.ts:2548RESPONSES_TURN_SEPARATOR→src/server.ts:2576out.push({ id:acp_turn_sep_${sepSeq}, role: "user", contentType: "text", ... }). - The compress-loop re-request has the same blind spot:
src/loop/core.ts:911adapter.buildRequest(...)→src/loop/core.ts:915normalizeStrictEchoBody(newBody, ...); andsrc/loop/adapter-responses.ts:281-310(buildRequest) never writes a reasoning item —src/loop/adapter-responses.ts:308is just{ ...requestBody, input: finalInput }.
Net: on the Responses wire a folded conversation can ship function_call items with no preceding reasoning item, and nothing repairs it — the proxy only warns.
Repro
- Codex CLI 0.155.1,
wire_api = "responses", base URL pointed at bili (proxy mode), upstream a third-party gateway fronting a DeepSeek thinking model. - Let a session grow until the model calls
compress(here:compress requested 2 range(s): m00875–m01151, m01153–m01187, ~52.5k tokens saved). - The round-2 re-request after the fold goes out with
reasoning-pair-violated: 4 function_call item(s) lack a preceding reasoning item while 2 existand is rejected 400.
Log (scrubbed), 05:56:40 → 05:57:14:
[acp-usage] round 2 input=48715 cached=45696 (cache hit 94%) fold=new
stripped 17 ACP status marker line(s) from incoming history
processTurn: 26 msgs, renderTags=text-only, 5 text tagged, 0 tool tagged
reasoning-pair-violated: 4 function_call item(s) lack a preceding reasoning item while 2 exist — strict-echo upstreams will reject the request (#684)
request: 1208 msgs, inbound=3.9MiB, outbound=222.7KiB, local=74ms
forward POST → https:///v1/responses
← upstream 400: {"error":{"message":"{"code":11155,"msg":"the reasoning content from the previous turn must be passed back in thinking mode", ... "extError":{"code":"reasoning_content_missing", ... "StatusCode":400}}","type":"api_error","code":"invalid_request_error"}}
Same shape at 04:54:07 (4 vs 2), 05:07:24 (5 vs 3), 05:37:15 (3 vs 2), 05:47:05 (5 vs 5).
learn-on-400 does not help. At 04:29:03 the session learned the flag (upstream 400 mentions reasoning_content — learned strictReasoningEcho for this session (#684/#762); reasoning-drop disabled) and was rejected 5 more times afterwards (04:54, 05:07, 05:37, 05:47, 05:57) — the flag only routes into normalizeStrictEchoReasoning, which this wire never reaches.
Not a version artifact. The failing process was 0.1.158; src/strict-echo.ts in 0.1.158, 0.1.159 and master is byte-identical, and 0.1.158's bundle already contains normalizeStrictEchoReasoning, normalizeStrictEchoBody, warnResponsesReasoningPairs and repairResponsesAssistantOrdering. 0.1.159 changes nothing on this path.
Request
- Give the Responses wire a repair, not just a canary. Either
- a
normalizeStrictEchoResponsesInputtwin that walksinput[]and inserts{"type":"reasoning","summary":[{"type":"summary_text","text":""}]}before afunction_callthat has none, gated by the sameisStrictReasoningEcho(...); or - make the fold rebuild keep/emit a reasoning item for each assistant run it re-anchors, instead of only the
[The exchange between these two assistant turns was compressed.]user separator.
- a
- If a blank reasoning item is not acceptable to every Responses upstream, at least preserve the provider-issued
reasoningitems of surviving turns through the fold (they exist in the inbound body — see Preserve provider-issued reasoning IDs in Responses input sanitization #1474 for the adjacent id-sanitization problem). - Optional: make the Responses canary name the index of the orphan
function_call, so the next report can point at a specific fold edge.
Not claimed
- I do not claim a blank-text reasoning item is accepted by every gateway; the blank form is what the 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 fix already relies on for chat-completions.
- I cannot reproduce on demand: 7 rejections across ~850 forwarded requests in one ~6.5-hour log, and they are not monotonic with the warning count (27 orphans at 05:46:31 passed; 4 at 05:57:14 failed) — whether the gateway enforces the check appears to vary.
reasoning-pair-violatedalso fires on many healthy requests, so the warning alone cannot serve as the repro signal.
Environment
- bili version: 0.1.158 (0.1.159 installed, not yet restarted — see "Not a version artifact")
- Usage mode: Proxy mode (client manually points its base URL at bili)
- Client and version: codex-cli 0.155.1
- OS: Windows 11 Pro for Workstations (win32 10.0.28000), x64
- Upstream and model: third-party gateway → DeepSeek thinking model (
deepseek-v4.1-flash);wire_api = "responses",model_reasoning_effort = "max"
bili version *
0.1.158
Usage mode *
Proxy mode (client manually points its base URL at bili)
Client and version *
codex-cli 0.155.1
Operating system *
Windows 11 Pro for Workstations (win32 10.0.28000), x64
Upstream and model *
third-party gateway → deepseek-v4.1-flash (thinking mode), wire_api=responses
Compression-related config (optional)
Log excerpt (strongly recommended)
[acp-usage] round 2 input=48715 cached=45696 (cache hit 94%) fold=new
stripped 17 ACP status marker line(s) from incoming history
processTurn: 26 msgs, renderTags=text-only, 5 text tagged, 0 tool tagged
reasoning-pair-violated: 4 function_call item(s) lack a preceding reasoning item while 2 exist — strict-echo upstreams will reject the request (#684)
request: 1208 msgs, inbound=3.9MiB, outbound=222.7KiB, local=74ms
forward POST → https://<gateway>/v1/responses
← upstream 400: {"error":{"message":"{"code":11155,"msg":"the reasoning content from the previous turn must be passed back in thinking mode", ... "extError":{"code":"reasoning_content_missing", ... "StatusCode":400}}","type":"api_error","code":"invalid_request_error"}}
/acp or acp_status output (optional)
Activity
ranxianglei commentedon Sep 27, 2026
[bot] 🏷 Triage verdict: confirmed, root cause verified at code level. Working on it — PR to follow in this thread.
What I verified (against master
7c679b6f):normalizeStrictEchoReasoninginsideprepareOpenai(src/server.ts:3127). Nothing equivalent exists on the Responses path.normalizeStrictEchoBody(src/strict-echo.ts) guards withif (!enabled || !Array.isArray(body.messages)) return body;— a Responses-wire re-request body carriesinput[], so it is a guaranteed no-op there. This matches your log exactly: 7 folds, 7 rejections, 0×strict-echo: injected.adapter-responses.buildRequestfrom the kernel view (coreToResponses/patchResponsesInput); a folded-away or blank reasoning item is simply not re-emitted, while its siblingfunction_callsurvives — the same producer mechanism as 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, just on the other wire. So this is a wire-coverage gap in the repair, not a fold/kernel bug: the fold legitimately drops blank echoes (kernel round-trip), and the Responses path has no compensating insertion.warnResponsesReasoningPairs(src/server.ts:2504) uses immediate-precedence semantics — it resets the "preceded by reasoning" flag on every non-reasoning item, so a perfectly healthy[reasoning, message, function_call]turn gets flagged. That explains the 798 hits including passing requests.Approach (your options 1 + 3, option 2 declined):
normalizeStrictEchoResponsesInputinsrc/strict-echo.ts— inserts one blank{type:"reasoning",summary:[{type:"summary_text",text:""}]}at the start of each assistant run that contains afunction_callbut no reasoning item (thinking-session gated, mirrors the canary). Wired into both outbound paths:prepareResponsesmain path (before the canary, so repaired bodies don't self-fire) and the loop re-request via extendingnormalizeStrictEchoBodyto handleinput[]. SameisStrictReasoningEchogate (learned flag / deepseek origin / deepseek model id).reasoning/assistant-message/function_call/custom_tool_callstretch; violation = run has a call but zero reasoning items) and name the orphanedcall_ids/indexes. This kills the chronic false positives and stays accurate on repaired bodies.reasoning-pair-violatedsentinel changes from immediate-precedence to run-based semantics — it will fire less often (healthy[reasoning, message, function_call]turns no longer flagged). Log prefix unchanged for grep continuity.中文摘要:确认了 Responses 线路缺少 #762 的 strict-echo 修复(修复只挂在 chat-completions 路径上,fold 后重发请求不带空 reasoning 导致网关 400),将按方案 1+3 实现(新增 Responses 版空推理注入 + 哨兵改为按回合判定),可以合并前会附回归测试。
ranxianglei commentedon Sep 27, 2026
[bot] 🏷 已修复 — PR: #1482 (
Fixes #1479)Triage verdict (verified at code level before implementing):
normalizeStrictEchoReasoningran inprepareOpenaionly;normalizeStrictEchoBodyguarded onArray.isArray(body.messages)so it no-opped on Responses bodies;prepareResponsesnever normalized. Your log signature matches exactly — 0×strict-echo: injected, 798×reasoning-pair-violated.What shipped (options 1+3):
normalizeStrictEchoResponsesInputinsrc/strict-echo.ts: injects one blank{type:"reasoning", summary:[{type:"summary_text", text:""}]}at the start of each assistant run carrying a tool call but no reasoning item — same gate as the chat path (isStrictReasoningEcholearned flag / deepseek statics + ≥1 reasoning item present, so non-thinking sessions stay byte-identical).normalizeStrictEchoBodynow patchesinput[]too, covering the loop re-request.prepareResponsesrepairsrebuiltInputbefore forwarding, ordered ahead of the canary so a repaired body can't self-fire its own sentinel.function_callids.Behavior changes (flagged per policy):
reasoning-pair-violatedfires LESS often: old check flagged everyfunction_callnot immediately preceded by reasoning (your healthy[reasoning, message, function_call]turns — most of the 798× noise); new check flags runs with tool calls but no reasoning at all. Log prefix kept for grep continuity.Evidence: new full-arc e2e test (
tests/loop-strict-echo-responses.test.ts) drives the real proxy on/v1/responses: req1 ships the orphan run → 400 → flag learned → main-path forward repaired → loop re-request (compress ack pair forms its own run) repaired; asserts zero orphan runs in both bodies and exactly one pre-repair sentinel hit. Unit suite extended for injector + new canary semantics. Pre-flight on head34b2970f: typecheck clean · 2918 pass / 0 fail / 12 skip · build OK.中文摘要:修复了 #762 严格回显补丁只覆盖 chat-completions、Responses 线路在 compress 折叠后仍因缺少 reasoning 被网关 400 的问题(新增 input[] 空白 reasoning 注入 + 主路径/循环重请求全接线),并把 canary 从"紧邻前驱"改为"按轮次"统计以消除健康请求的误报,可以合并。
fix: extend strict-echo repair to the Responses wire (#1479)
fix(review): complete inRun set + WC-009 wire-contract ledger entry
fix(review): complete canary inRun set for custom_tool_call_output (#…
Merge pull request #1482 from ranxianglei/2026-09-27_responses-strict…
test(strict-echo): pin the redacted real-world #2169 body as a regres…
fix(strict-echo): clear blank reasoning content + learn reasoning_tex…