Repository navigation
Description
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.
-
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.
-
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:
- Context size alone is not a clean trigger. The first time a single phrase crossed 50 repeats, packed context was 200k, 342k and 362k in the three sessions respectively. The sessions that got furthest also grew largest, but small-by-comparison sessions still looped.
- It clusters around long sessions with heavy tool use, which is consistent with reasoning: model reasoning output stuck in infinite repetitive loop #44962 and provider/streaming: reasoning degenerates into repetitive loop on long session #51503.
- It does not self-correct. Once it starts, the only thing that reliably broke it was injecting a new user message. Manually stopping the turn and resuming produced the loop again immediately.
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:
- 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. - 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 back400 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
- 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?
- Is there any server-side repetition guard today? If the upstream already reports
repetition_loopinfinish_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
opencode-agent commentedon Oct 15, 2026
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 back400, which looks new. We can't reproduce that one yet. Could you add:/compact, resumed a session, switched variant), what you expected, and what happened./chat/completionsor/responses)reasoning_content,reasoning_text,reasoning, or a reasoning item), and whether any assistant tool-call turn has none at allreasoning_effort(or a similar field) is setYou 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:
deepseek-v4-flash/deepseek-v4.1-flashwhen the request both echoes back reasoning and setsreasoning_effort. One commenter sees it after repeated/compact.deepseek-v4-flashreturns a 400 when continuing a long session, and it depends on the session's history.reasoning_content ... must be passed back to the API400s on DeepSeek V4 (Flash) through Zen/Go.finish_detailssuch asrepetition_loop.hhtbing commentedon Oct 15, 2026
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:
finish: error,error.type: provider.invalid-request).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:
/v1/chat/completions. This is the important one. The model in question is not served over/responses— it is classified aschat, so every request goes through a chat-completions bridge.reasoning_content/reasoning_text/reasoningfield 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:
delta.reasoning_content/delta.reasoningchunks 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.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:
/responsesmodelsAsk
If the bridge is dropping
reasoning_contenton 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.
opencode-agent commentedon Oct 15, 2026
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):delta.reasoning_content/delta.reasoning/delta.reasoning_textchunks are read and kept as reasoning parts. They aren't counted and discarded.deepseek, OpenCode always replaysreasoning_contenton 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.chatand converts between/responsesand 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/responsesmodels work.Could you check:
opencodeCLI talking directly to OpenCode Zen (no proxy inbaseURL, no desktop client), on a long tool-heavy session withdeepseek-v4.1-flash?reasoning_contentis present, with content redacted)./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.
hhtbing commentedon Oct 15, 2026
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:
finish: error,error.type: provider.invalid-request).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:
/v1/chat/completions. This is the important one. The model in question is not served over/responses— it is classified aschat, so every request goes through a chat-completions bridge.reasoning_content/reasoning_text/reasoningfield 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:
delta.reasoning_content/delta.reasoningchunks 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.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:
/responsesmodelsAsk
If the bridge is dropping
reasoning_contenton 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.
opencode-agent commentedon Oct 15, 2026
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_contentwhen sent back, even if it's emptychatand converts between/responsesand chat completionsSo 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:
opencodeCLI talking directly to OpenCode Zen, without the proxy or the desktop client?reasoning_contentpresent on the earlier assistant messages? Content can be redacted.Martino17x commentedon Oct 16, 2026
Independent reproduction of the
reasoning_text must be passed back400 — Codex CLI on/v1/responses, no local proxyAdding 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
0.162.0-alpha.17.2(Codex Desktop26.1007.9446.0), Windows 11POST https://opencode.ai/zen/go/v1/responses(native Responses wire API,wire_api = "responses")deepseek-v4.1-flashmodel_reasoning_effort = "max"disable_response_storage = true(client replays full history; noprevious_response_id)base_url = "https://opencode.ai/zen/go/v1", no proxy, no router, no aggregation127.0.0.1:8787but the config does NOT point at it, and its log shows 0 requests receivedRepro
Nothing exotic: an ordinary long agent session with many tool calls. The turn following a batch of tool calls fails.
Observed 6 times inside ~16 minutes, all within the same session:
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
reasoningvs. lines containing the 400 text, per day:reasoningmentionsmust be passed backSame 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_effortanddisable_response_storageare 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
deepseek-v4.1-flashis a chat model on Zen, so a/v1/responsesrequest 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
maxis the correct top level for this model on this endpoint (theOpenCode Gopreset in the CC Switch switcher documentsdeepseek-v4-flashas["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.opencode-agent commentedon Oct 18, 2026
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_contentis sent back on earlier assistant turns. The bridge you described (the model labelledchat, 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 back400 also happens with only theopencodeCLI talking directly to OpenCode Zen, please comment here or open a new issue with: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.