Skip to content

[Bug] #762's strict-echo repair is chat-completions-only: Responses-wire requests still 400 with reasoning_content_missing after a compress fold #1479

Closed

Description

@cyberlieflife

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 compress fold.
  • strict-echo: injected fired 0 times in the whole 7,707-line log, while reasoning-pair-violated fired 798 times.

Why the #762 fix cannot fire here

master @ 54c433a1 (v0.1.159):

  • src/strict-echo.ts:35 normalizeStrictEchoReasoning(messages, enabled, log, sessionId) injects reasoning_content: "" into assistant tool-call messages lacking the field.
  • src/strict-echo.ts:61-67 normalizeStrictEchoBody guards with if (!enabled || !Array.isArray(body.messages)) return body; — a Responses body carries input[], not messages[], so this is a guaranteed no-op on this wire.
  • src/server.ts:3146 is the only call site, inside prepareOpenai (chat-completions).
  • The Responses path (prepareResponses, src/server.ts:3399) does the opposite of repairing: src/server.ts:3660 builds { ...parsed, input: rebuiltInput, tools: toolsOut } and src/server.ts:3661 merely calls warnResponsesReasoningPairs(...) — a log("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:2548 RESPONSES_TURN_SEPARATOR → src/server.ts:2576 out.push({ id: acp_turn_sep_${sepSeq}, role: "user", contentType: "text", ... }).
  • The compress-loop re-request has the same blind spot: src/loop/core.ts:911 adapter.buildRequest(...) → src/loop/core.ts:915 normalizeStrictEchoBody(newBody, ...); and src/loop/adapter-responses.ts:281-310 (buildRequest) never writes a reasoning item — src/loop/adapter-responses.ts:308 is 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

  1. 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.
  2. Let a session grow until the model calls compress (here: compress requested 2 range(s): m00875–m01151, m01153–m01187, ~52.5k tokens saved).
  3. 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 exist and 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

  1. Give the Responses wire a repair, not just a canary. Either
    • a normalizeStrictEchoResponsesInput twin that walks input[] and inserts {"type":"reasoning","summary":[{"type":"summary_text","text":""}]} before a function_call that has none, gated by the same isStrictReasoningEcho(...); 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.
  2. If a blank reasoning item is not acceptable to every Responses upstream, at least preserve the provider-issued reasoning items 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).
  3. 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

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

  1. ranxianglei commented on Sep 27, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [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):

    1. 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 chat-completions repair has exactly one call site: normalizeStrictEchoReasoning inside prepareOpenai (src/server.ts:3127). Nothing equivalent exists on the Responses path.
    2. The loop-side hook normalizeStrictEchoBody (src/strict-echo.ts) guards with if (!enabled || !Array.isArray(body.messages)) return body; — a Responses-wire re-request body carries input[], so it is a guaranteed no-op there. This matches your log exactly: 7 folds, 7 rejections, 0× strict-echo: injected.
    3. The loop re-request is built by adapter-responses.buildRequest from the kernel view (coreToResponses/patchResponsesInput); a folded-away or blank reasoning item is simply not re-emitted, while its sibling function_call survives — 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.
    4. Bonus finding confirmed: 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):

    • Option 1: new normalizeStrictEchoResponsesInput in src/strict-echo.ts — inserts one blank {type:"reasoning",summary:[{type:"summary_text",text:""}]} at the start of each assistant run that contains a function_call but no reasoning item (thinking-session gated, mirrors the canary). Wired into both outbound paths: prepareResponses main path (before the canary, so repaired bodies don't self-fire) and the loop re-request via extending normalizeStrictEchoBody to handle input[]. Same isStrictReasoningEcho gate (learned flag / deepseek origin / deepseek model id).
    • Option 3: rewrite the canary to run-based semantics (a run = maximal consecutive reasoning/assistant-message/function_call/custom_tool_call stretch; violation = run has a call but zero reasoning items) and name the orphaned call_ids/indexes. This kills the chronic false positives and stays accurate on repaired bodies.
    • Option 2 (preserving provider-issued reasoning items across folds) is a much larger kernel-view surgery and not needed for correctness: the strict-echo contract is satisfied by the blank echo, exactly as proven by 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 on the chat wire. Happy to file it separately if you want the fidelity improvement tracked.

    ⚠️ Behavior change (flagging per policy): the reasoning-pair-violated sentinel 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 版空推理注入 + 哨兵改为按回合判定),可以合并前会附回归测试。

  2. ranxianglei commented on Sep 27, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已修复 — PR: #1482 (Fixes #1479)

    Triage verdict (verified at code level before implementing):

    • Confirmed: normalizeStrictEchoReasoning ran in prepareOpenai only; normalizeStrictEchoBody guarded on Array.isArray(body.messages) so it no-opped on Responses bodies; prepareResponses never normalized. Your log signature matches exactly — 0× strict-echo: injected, 798× reasoning-pair-violated.
    • Root cause layer: the repair mechanism existed but was wire-scoped to chat-completions; the fold geometry then produced orphan runs (assistant run with tool calls, no reasoning item) that nothing on the Responses path repaired. Also found while in there: the loop's truncation/degenerate retries were never strict-echo-normalized even on the chat wire — same root cause, closed in the same PR.
    • Your option 2 (preserve provider reasoning across folds) I deliberately did NOT take: it changes kernel-fold semantics and isn't required once the blank-echo twin exists — the gateway's requirement ("pass back something for that turn") is satisfied by the blank item. Happy to file it separately if you want the fidelity angle tracked.

    What shipped (options 1+3):

    1. New normalizeStrictEchoResponsesInput in src/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 (isStrictReasoningEcho learned flag / deepseek statics + ≥1 reasoning item present, so non-thinking sessions stay byte-identical). normalizeStrictEchoBody now patches input[] too, covering the loop re-request.
    2. prepareResponses repairs rebuiltInput before forwarding, ordered ahead of the canary so a repaired body can't self-fire its own sentinel.
    3. Canary rewritten immediate-precedence → run-based, now naming the orphaned function_call ids.

    Behavior changes (flagged per policy):

    • Responses-wire outbound bodies change when strict-echo applies: blank reasoning items appear at orphan run starts (previously absent → the 400s). Non-strict origins / non-thinking sessions untouched.
    • reasoning-pair-violated fires LESS often: old check flagged every function_call not 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.
    • Loop retry paths (truncation/degenerate) are now normalized too — previously unnormalized on every wire.

    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 head 34b2970f: typecheck clean · 2918 pass / 0 fail / 12 skip · build OK.

    中文摘要:修复了 #762 严格回显补丁只覆盖 chat-completions、Responses 线路在 compress 折叠后仍因缺少 reasoning 被网关 400 的问题(新增 input[] 空白 reasoning 注入 + 主路径/循环重请求全接线),并把 canary 从"紧邻前驱"改为"按轮次"统计以消除健康请求的误报,可以合并。

  3. added a commit that references this issue on Oct 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

    Labels

    bugSomething isn't workingp1

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions