Skip to content

reasoning: repetition loop on deepseek-v4.1-flash, plus a paired reasoning_text must be passed back 400 #55584

Closed as not planned

Description

@hhtbing

Reasoning repetition loop on deepseek-v4.1-flash, plus a paired reasoning_text must be passed back 400

Summary

I hit two failure modes on deepseek-v4.1-flash in thinking mode, and I believe they are two different bugs that happen to co-occur on long sessions.

  1. The repetition loop already tracked in reasoning: model reasoning output stuck in infinite repetitive loop #44962 / Let me do it. Emitting. Let me test. Emitting. LOOP #50634 / provider/streaming: reasoning degenerates into repetitive loop on long session #51503. This report adds measured data across three abandoned sessions: where it starts, how big the context was, and what actually breaks it.

  2. A paired HTTP 400 that I could not find documented anywhere in this repo:

    The `reasoning_text` in the thinking mode must be passed back to the API.
    

    It fires in sessions as small as ~75k packed tokens, so it is not a context-length problem, and it looks like OpenCode building a request where a reasoning item and its text are out of sync. That is why I am filing it rather than treating it as another symptom of (1).

Environment

item value
opencode 2.0.28 when observed, re-checked on 2.0.29
OS Linux x86_64
Client third-party desktop client driving the OpenCode server, and the opencode CLI against the same session (both reproduce)
Model deepseek-v4.1-flash, thinking on, variant medium
Provider OpenCode Zen
Proxy a local OpenAI-compatible passthrough in front of Zen. It does not touch reasoning items (no reasoning_text handling in its source), so the request body Zen receives is the body OpenCode assembled

Observation 1 — repetition loop, with measurements

The reasoning stream collapses into one short filler phrase repeated thousands of times inside a single turn. No assistant message is ever emitted, the session is effectively hung, and it has to be abandoned.

The repeated segment is consistently a 1–4 character Chinese filler. The top segment is always 好。 ("okay."), followed by 执行。 ("execute."). That matches the 好。 reports in #51503.

Measured by parsing the persisted session store, three abandoned sessions:

session assistant msgs peak packed tokens peak @ seq max repeats of one phrase turn outcome
A 357 531,938 3221 5,867 aborted by user
B 381 517,473 4094 6,604 aborted by user
C 289 348,184 1940 6,699 aborted by user

Packed tokens = input + output + reasoning + cache.read + cache.write.

Three things worth noting:

Observation 2 — the reasoning_text 400

Full upstream payload, verbatim (id redacted):

{
  "model": "deepseek-v4.1-flash",
  "error": {
    "message": "The `reasoning_text` in the thinking mode must be passed back to the API. (request_id: <redacted>)",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_request_error"
  }
}

Persisted shape: finish: "error", error.type: "provider.invalid-request", HTTP 400.

Counted over the whole local session store: 66 occurrences across 9 distinct sessions. Two of them are small, early, unrelated sessions:

session total messages max packed tokens in session
small session X 26 ~87k
small session Y 28 ~112k

So this 400 fires in sessions that are nowhere near any window limit.

Two things separate it from a context overflow:

  1. It is not context_length_exceeded. On the same provider, requests above roughly 530k packed tokens were rejected with a normal overflow error. This 400 fires well below that, and in sessions under 120k.
  2. It clusters right after compaction or a long-session resume, which points at the assistant reasoning item being reconstructed and then replayed in a shape the upstream rejects. Most likely a reasoning item sent without its text, or text sent without the matching item id, so the two halves of one item get split apart.

Why file this separately

#44962 / #50634 / #51503 cover the loop. What I could not find documented anywhere is:

  • the paired reasoning_text must be passed back 400 on the same long sessions, and
  • that a single new user message reliably breaks the loop, while the 400 keeps recurring after the loop is broken.

#53489 is directly related: it reported that server-side finish_details (such as repetition_loop) is not surfaced. Surfacing it, or aborting on it, would turn an unbounded token burn into a clean, catchable error instead of a stream that never ends.

Ask

  1. On the Responses path, when a prior assistant turn contains a reasoning item, is it expected to be echoed back verbatim? If so, the 400 suggests OpenCode can assemble a request where the reasoning text and its item envelope get out of sync. Is that a known issue, and is there a known input shape that triggers it?
  2. Is there any server-side repetition guard today? If the upstream already reports repetition_loop in finish_details, could OpenCode surface it, or abort the turn, instead of streaming the loop to completion?

I can provide the raw session rows (assistant, tool and compaction records, with model and token fields) if that helps. I will redact anything else needed.

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed data. The repetition loop is already tracked in #44962, so it would help to keep this issue about the reasoning_text must be passed back 400, which looks new. We can't reproduce that one yet. Could you add:

    1. In a few sentences of your own, what you did right before the 400 (e.g. ran /compact, resumed a session, switched variant), what you expected, and what happened.
    2. The request body that got the 400. Your local passthrough sees exactly what OpenCode sends, so if you can log one failing request (secrets and file contents redacted), that's the most useful thing. In particular:
      • which endpoint it went to (/chat/completions or /responses)
      • how the earlier assistant messages carry their reasoning (reasoning_content, reasoning_text, reasoning, or a reasoning item), and whether any assistant tool-call turn has none at all
      • whether reasoning_effort (or a similar field) is set
    3. Whether the same session works again after you switch to a different variant, or to a model other than DeepSeek, without changing anything else.

    You can reply here or edit the issue. If you edit it, please also leave a comment saying you've updated it, so we know you're done.

    Related issues, ordered from most to least likely to match:

  2. hhtbing commented on Oct 15, 2026

    @hhtbing
    Author

    Thanks. Answers to your three questions, plus one thing I found that I think is the actual trigger.

    1. What I did right before the 400

    Nothing exotic. It fires mid-turn on an ordinary session, most often on a turn that follows a lot of tool calls. Concretely:

    • The session keeps working normally for many turns.
    • Then a turn comes back with HTTP 400 and the body below, and the whole turn is lost (finish: error, error.type: provider.invalid-request).
    • Sending any new user message afterwards usually lets the session continue; the 400 is not sticky per-session.

    I did not run /compact, did not resume, and did not switch variant right before the first one. It also happened in short sessions that were nowhere near any context limit (measured: one session at ~87k packed tokens, another at ~112k).

    2. The request body

    I can't paste the raw body here, but I can give you the two facts you asked for that decide this, plus the code path:

    • Endpoint: /v1/chat/completions. This is the important one. The model in question is not served over /responses — it is classified as chat, so every request goes through a chat-completions bridge.
    • How earlier turns carry reasoning: the bridge drops reasoning items entirely on the request side. There is no reasoning_content / reasoning_text / reasoning field sent back for prior assistant turns, and no reasoning item either. Assistant tool-call turns therefore carry no reasoning at all.

    So on this path the request contains neither of the two fields that #48180 says trigger it. That makes me doubt this is the same bug as #48180 for my case specifically.

    3. Does it work after switching variant or model?

    Yes — this is the strongest signal I have. Switching to any model served over /responses (instead of the chat bridge) makes the same session work again without changing anything else. Switching variant alone on the same model did not help.

    The thing I think is actually happening

    The 400 fires on the response side, not because we sent something wrong, but because the upstream expects reasoning to be echoed back on this path and we never send it.

    On the bridge:

    • Response side: incoming delta.reasoning_content / delta.reasoning chunks are counted and discarded — they never reach the client. In my logs this happens on essentially every turn, and one turn dropped 1,402 reasoning chunks.
    • Request side: because those reasoning chunks were never surfaced, there is nothing to echo back on the next request.

    So the upstream sees a thinking-mode model being driven with its reasoning silently stripped in both directions. That matches the error text exactly: it is the upstream telling us reasoning must be passed back, on a request where we structurally cannot pass it back.

    Two numbers that support this:

    observation value
    occurrences of the 400 in my session store 66, across 9 sessions
    smallest session that hit it ~87k packed tokens (nowhere near a limit)
    reasoning chunks dropped in a single turn, worst case 1,402
    whether it happens on /responses models no

    Ask

    If the bridge is dropping reasoning_content on both directions for a thinking-mode model, is that expected? And if a model is served over chat/completions where reasoning can't round-trip, should it be routed with thinking disabled instead, so the upstream never asks for reasoning to be passed back?

    I can provide raw session rows (assistant/tool/compaction records with model and token fields) if that helps, with anything else redacted.

  3. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks, that narrows it down a lot. The "bridge" you describe doesn't match how OpenCode handles this model, though, so I think the reasoning is being lost before OpenCode is involved.

    In OpenCode 2.0.29 (packages/ai/src/protocols/openai-chat.ts):

    • Response side: streamed delta.reasoning_content / delta.reasoning / delta.reasoning_text chunks are read and kept as reasoning parts. They aren't counted and discarded.
    • Request side: for any model whose ID contains deepseek, OpenCode always replays reasoning_content on earlier assistant messages. A tool-call turn with no stored reasoning gets "reasoning_content": "", specifically so DeepSeek thinking mode doesn't reject the request. OpenCode never sends a DeepSeek assistant turn without that field.
    • OpenCode has no layer that labels a model chat and converts between /responses and chat completions. A model is set up to use one protocol or the other.

    What you describe (a model classified as chat, a bridge that counts and drops reasoning chunks, nothing echoed back) sounds like the local OpenAI-compatible passthrough or the third-party desktop client. Your original report said the proxy doesn't touch reasoning. If the bridge is one of those two, that would explain both the 400 and why /responses models work.

    Could you check:

    1. Which component the "bridge" and the dropped-chunk counts come from: OpenCode's own logs, your proxy, or the desktop client?
    2. Does the 400 still happen with only the opencode CLI talking directly to OpenCode Zen (no proxy in baseURL, no desktop client), on a long tool-heavy session with deepseek-v4.1-flash?
    3. If it does: the model ID shown in the CLI, and one failing request taken from OpenCode's side before any proxy (just the shape of the assistant messages, i.e. whether reasoning_content is present, with content redacted).
    4. Which model or protocol you switched to when it worked again ("served over /responses").

    If the plain CLI going straight to Zen doesn't show the 400, the fix belongs in the bridge rather than in OpenCode. #48180 is still the closest OpenCode-side report for the same model on Zen, and #44962 tracks the reasoning repetition loop.

  4. hhtbing commented on Oct 15, 2026

    @hhtbing
    Author

    Thanks. Answers to your three questions, plus one thing I found that I think is the actual trigger.

    1. What I did right before the 400

    Nothing exotic. It fires mid-turn on an ordinary session, most often on a turn that follows a lot of tool calls. Concretely:

    • The session keeps working normally for many turns.
    • Then a turn comes back with HTTP 400 and the body below, and the whole turn is lost (finish: error, error.type: provider.invalid-request).
    • Sending any new user message afterwards usually lets the session continue; the 400 is not sticky per-session.

    I did not run /compact, did not resume, and did not switch variant right before the first one. It also happened in short sessions that were nowhere near any context limit (measured: one session at ~87k packed tokens, another at ~112k).

    2. The request body

    I can't paste the raw body here, but I can give you the two facts you asked for that decide this, plus the code path:

    • Endpoint: /v1/chat/completions. This is the important one. The model in question is not served over /responses — it is classified as chat, so every request goes through a chat-completions bridge.
    • How earlier turns carry reasoning: the bridge drops reasoning items entirely on the request side. There is no reasoning_content / reasoning_text / reasoning field sent back for prior assistant turns, and no reasoning item either. Assistant tool-call turns therefore carry no reasoning at all.

    So on this path the request contains neither of the two fields that #48180 says trigger it. That makes me doubt this is the same bug as #48180 for my case specifically.

    3. Does it work after switching variant or model?

    Yes — this is the strongest signal I have. Switching to any model served over /responses (instead of the chat bridge) makes the same session work again without changing anything else. Switching variant alone on the same model did not help.

    The thing I think is actually happening

    The 400 fires on the response side, not because we sent something wrong, but because the upstream expects reasoning to be echoed back on this path and we never send it.

    On the bridge:

    • Response side: incoming delta.reasoning_content / delta.reasoning chunks are counted and discarded — they never reach the client. In my logs this happens on essentially every turn, and one turn dropped 1,402 reasoning chunks.
    • Request side: because those reasoning chunks were never surfaced, there is nothing to echo back on the next request.

    So the upstream sees a thinking-mode model being driven with its reasoning silently stripped in both directions. That matches the error text exactly: it is the upstream telling us reasoning must be passed back, on a request where we structurally cannot pass it back.

    Two numbers that support this:

    observation value
    occurrences of the 400 in my session store 66, across 9 sessions
    smallest session that hit it ~87k packed tokens (nowhere near a limit)
    reasoning chunks dropped in a single turn, worst case 1,402
    whether it happens on /responses models no

    Ask

    If the bridge is dropping reasoning_content on both directions for a thinking-mode model, is that expected? And if a model is served over chat/completions where reasoning can't round-trip, should it be routed with thinking disabled instead, so the upstream never asks for reasoning to be passed back?

    I can provide raw session rows (assistant/tool/compaction records with model and token fields) if that helps, with anything else redacted.

  5. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks. This looks like the same comment I replied to just above, so that reply still applies. In short, in OpenCode 2.0.29 reasoning isn't dropped for this model in either direction:

    • reasoning chunks received from the model are kept, not counted and discarded
    • earlier assistant turns always carry reasoning_content when sent back, even if it's empty
    • OpenCode has no step that labels the model chat and converts between /responses and chat completions

    So the "bridge" you describe is most likely your passthrough proxy or the desktop client.

    To move this forward, please answer the questions in that reply. Most important:

    1. Which component do the dropped-chunk counts and the bridge come from: OpenCode's own logs, your proxy, or the desktop client?
    2. Does the 400 still happen with only the opencode CLI talking directly to OpenCode Zen, without the proxy or the desktop client?
    3. If it does, please describe one failing request as OpenCode sends it, before it reaches any proxy: is reasoning_content present on the earlier assistant messages? Content can be redacted.
  6. Martino17x commented on Oct 16, 2026

    @Martino17x

    Independent reproduction of the reasoning_text must be passed back 400 — Codex CLI on /v1/responses, no local proxy

    Adding a second data point to this issue, from a different client stack than the original report.

    Why this may help your open questions

    You asked whether the "bridge that drops reasoning" was OpenCode, the reporter's passthrough, or the desktop client. My setup answers that from the other side: there is no local transformation layer at all. Codex CLI talks straight to https://opencode.ai/zen/go/v1. Nothing between the client and Zen touches the payload. The 400 still fires.

    So whatever drops the reasoning on replay, in my case it cannot be a client-side bridge.

    Environment

    item value
    Client Codex CLI 0.162.0-alpha.17.2 (Codex Desktop 26.1007.9446.0), Windows 11
    OS Windows 11 Pro, build 26200 (25H2)
    Endpoint POST https://opencode.ai/zen/go/v1/responses (native Responses wire API, wire_api = "responses")
    Model deepseek-v4.1-flash
    Reasoning model_reasoning_effort = "max"
    Storage disable_response_storage = true (client replays full history; no previous_response_id)
    Provider entry direct — base_url = "https://opencode.ai/zen/go/v1", no proxy, no router, no aggregation
    Local proxies a local shim exists on 127.0.0.1:8787 but the config does NOT point at it, and its log shows 0 requests received

    Repro

    Nothing exotic: an ordinary long agent session with many tool calls. The turn following a batch of tool calls fails.

    The `reasoning_text` in the thinking mode must be passed back to the API.
    

    Observed 6 times inside ~16 minutes, all within the same session:

    # timestamp (UTC) timestamp (local, UTC-3)
    1 2026-10-16 02:23:22 2026-10-15 23:23:22
    2 2026-10-16 02:24:43 2026-10-15 23:24:43
    3 2026-10-16 02:24:59 2026-10-15 23:24:59
    4 2026-10-16 02:29:28 2026-10-15 23:29:28
    5 2026-10-16 02:31:36 2026-10-15 23:31:36
    6 2026-10-16 02:39:11 2026-10-15 23:39:11

    It fires mid-turn and the whole turn is lost. Sending a new user message afterwards usually lets the session continue, so it is not sticky per session — same as the original report.

    Timeline evidence: this started today, and nothing on my side changed

    I keep local client logs and config backups, so the onset is measurable. Counts of log lines mentioning reasoning vs. lines containing the 400 text, per day:

    day (UTC) log lines reasoning mentions must be passed back
    2026-10-13 17,061 15,641 0
    2026-10-14 3,746 2,703 0
    2026-10-15 1,103 508 0
    2026-10-16 1,948 1,890 6

    Same client version, same provider, same model, same effort on the zero-error days. Config backup from 2026-10-09 vs. the config in use today: model_provider, base_url, wire_api, model, model_reasoning_effort and disable_response_storage are byte-identical. The only differences are unrelated MCP server blocks.

    I also have per-session records: the previous direct-mode sessions on 2026-10-13 and 2026-10-14 ran on the identical client version (0.162.0-alpha.17.2) with zero occurrences of this 400.

    Net: ~22k log lines of identical-config usage with zero hits through Oct 15, then 6 hits in the first 16 minutes of Oct 16.

    Hypotheses

    1. A gateway-side rollout around 2026-10-15/16 UTC. The onset is sharp and the local setup is provably unchanged. This is my leading hypothesis.
    2. The Responses → Chat translation path for DeepSeek. deepseek-v4.1-flash is a chat model on Zen, so a /v1/responses request must be translated. If the translation does not carry the reasoning back on the replayed assistant tool-call turn, DeepSeek rejects it — which is exactly what the error says. I cannot see inside that translation from the client side, so I cannot confirm it; that is the thing I would ask you to check.

    What I can provide

    I am happy to send a redacted failing request body. It needs a temporary logging passthrough in front of Zen to capture it (I have one; it is simply not in the path today), and I can run that on request and post the payload with secrets and file contents redacted.

    Small note on effort levels

    max is the correct top level for this model on this endpoint (the OpenCode Go preset in the CC Switch switcher documents deepseek-v4-flash as ["low","high","max"]), so I do not believe the effort value is the trigger. Mentioning it in case the relay clamps effort per model and that table matters here.

  7. opencode-agent commented on Oct 18, 2026

    @opencode-agent
    Contributor

    Closing this for now because we still couldn't tell which component is dropping the reasoning. In OpenCode 2.0.29, DeepSeek reasoning is kept when it streams in, and reasoning_content is sent back on earlier assistant turns. The bridge you described (the model labelled chat, reasoning chunks counted and discarded, nothing sent back) seems to be outside OpenCode, most likely the passthrough proxy or the desktop client.

    If the reasoning_text must be passed back 400 also happens with only the opencode CLI talking directly to OpenCode Zen, please comment here or open a new issue with:

    • the version
    • the model ID
    • whether earlier assistant messages in the failing request carry reasoning_content (content redacted)

    The repetition loop is tracked in #44962, and #48180 is the closest open report of a DeepSeek 400 on Zen. Thanks for the detailed measurements.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions