anomalyco / anomalyco/opencode
[Bug] MiniMax-M3 (reasoning model) silently finishes stop and produces no text/tool parts in OpenCode 1.18.18
@kitlangton is already working on this.
Since Aug 17, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
[Bug] MiniMax-M3 (reasoning model) silently finishes stop and produces no text/tool parts in OpenCode 1.18.18
Summary
When using MiniMax-M3 (provider minimax-cn-coding-plan, a reasoning-capable model via OpenAI-compatible /v1/chat/completions), OpenCode 1.18.18 sometimes saves an assistant message with finish: "stop", tokens counted, only a reasoning part and no text/tool parts at all, and then waits for the next user message. From the UI perspective the model "thinks and stops" with no visible reply and no tool calls.
The same task works perfectly with deepseek-v4-flash (also a reasoning-capable model through the OpenAI-compatible layer) under the same configuration.
This looks like a sibling of #42677 (and #42300, #42287): OpenCode's stream→parts materializer is dropping parts when the upstream model returns reasoning content embedded inside delta.content rather than as a separate delta.reasoning_content field.
Environment
- OpenCode: 1.18.18 (native Tauri build)
- OS: WSL2 (Ubuntu 24.04), Linux x86_64
- Provider:
minimax-cn-coding-plan(OpenAI-compatible,https://api.minimaxi.com/v1/chat/completions) - Model:
MiniMax-M3(advertised as reasoning-capable; OMO bundled snapshot marksreasoning: true) - Plugin:
oh-my-openagent4.19.0 (with PR #4719 fix already merged) - Agent:
Sisyphus - Ultraworker - Reproducible: yes, on demand (see below)
Reproduction
- In OpenCode Web UI, open a session using
Sisyphus - Ultraworkeragent with default modelminimax-cn-coding-plan/MiniMax-M3. - Send a coding investigation task that requires the agent to (a) reason about findings, (b) decide to call a tool, and (c) write a final text report. Example: ask the agent to check a backend file for an arithmetic bug and produce a diagnosis.
- Observe the agent's first turn: it may emit one
tool_callpart correctly, then on the subsequent turn produce an assistant message with onlyreasoningcontent and notextortoolparts. - The user prompt that reliably triggers it in our setup is "继续,把结论写出来" (continue, write the conclusion) after the agent has already found the relevant code.
- Switch the model dropdown to
deepseek-v4-flashand send the same prompt → the same agent emitsreasoning+text+toolparts normally and the conversation proceeds.
Symptom (DB evidence)
Database: ~/.local/share/opencode/opencode.db
A failing turn produced this single assistant message (excerpted from message + part tables; role/model/tokens come from message.data, part.data):
msg_id: msg_00eb4ce45001G3qHLlgD3hqm0N
time_created: 2026-08-17 15:52:05
data:
role: assistant
agent: Sisyphus - ultraworker
modelID: MiniMax-M3
providerID: minimax-cn-coding-plan
finish: stop
tokens: { total: 430932, input: 430177, output: 627, reasoning: 0,
cache: { write: 0, read: 128 } }
Parts in this message:
part[0] type=step-start text_len=0
part[1] type=reasoning text_len=1741 ← only reasoning, NO text, NO tool
part[2] type=step-finish text_len=0
The 1741-char reasoning content describes the model's internal plan — including a fully-formatted bash invocation it intends to make — but finish_reason is already stop, no tool_calls were emitted, and no assistant text reply exists.
For comparison, the same prompt with deepseek-v4-flash immediately afterwards produced three assistant messages, all of which had [reasoning, text, tool] parts and finish: tool-calls/stop properly sequenced:
[15:57:38] assistant finish=tool-calls out=399 types=[reasoning, text, tool]
[15:58:32] assistant finish=tool-calls out=613 types=[reasoning, text, tool]
[15:58:59] assistant finish=stop out=932 types=[text]
This is the exact same bug signature described in #42677 ("opencode run silently saves an empty assistant stop message (tokens counted, zero parts) and exits 0"). We are seeing it not with ollama but with MiniMax's hosted OpenAI-compatible endpoint, and not on opencode run but inside an interactive Tauri/WebUI session.
Evidence: MiniMax-M3 SSE stream format does not use delta.reasoning_content
Captured by hitting https://api.minimaxi.com/v1/chat/completions directly with a Bearer key from ~/.local/share/opencode/auth.json, model=MiniMax-M3, stream=true, prompt="计算 17*23 等于多少?只输出数字":
HTTP 200
Content-Type: text/event-stream; charset=utf-8
chunk [0]: delta = { role: "assistant" }
chunk [2]: delta.content = "The user is asking me to calculate 17 * 23 and only output the number.\n\n"
chunk [4]: delta.content = "17 * 23 = 17 * 20 + 17 * 3 = "
chunk [6]: delta.content = "340 + 51 = 391\n\nThe user wants only"
chunk [8]: delta.content = "the number as output."
chunk [10]: delta.content = "\n\n391" ← thinking closes, then the answer
chunk [12]: delta = { role: "assistant" }, finish_reason: "stop"
DeepSeek's stream under the same test uses the OpenAI-standard delta.reasoning_content field separately from delta.content (visible in chunked deltas when reasoning_effort is set).
MiniMax-M3 embeds its reasoning inside delta.content wrapped in <think>...</think> tags, with no reasoning_content field anywhere. When the same chunk stream also contains a planned tool_call, both end up serialized into the same delta.content blob with custom ]<minim>...<tool_call> pseudo-tags, which OpenCode 1.18.18 does not appear to parse as separate parts.
Hypothesized root cause
OpenCode's @ai-sdk/openai-compatible provider (or its downstream message materializer) assumes reasoning content arrives in delta.reasoning_content (the OpenAI standard since o1). For MiniMax-M3 the reasoning text arrives inside delta.content together with the answer and any pseudo-tool-call XML, so:
- The provider sees only one big
content stringand never constructs a dedicated reasoning part — but does happen to attach areasoningpart (1741 chars in our example), suggesting some heuristic extraction may be running partially. - The same
contentblob is not recognized as validtextbecause it contains…markers and pseudo-tool-call tags, so OpenCode declines to materialize atextpart. - No native
tool_callsare in the stream because MiniMax's pseudo-tool-call syntax is non-standard, so notoolpart is created either. - The stream ends with
finish_reason: "stop"and OpenCode writes the message with only the (half-recognized) reasoning part — which is exactly what we see.
Net effect from the user: the agent "thinks" for one round, then the UI shows nothing and the session stalls.
Why this is hard to detect automatically
- The response is HTTP 200, complete, and tokens are counted (
out=627in our run). - No error is raised; OpenCode simply believes the turn is finished.
- It only manifests with reasoning-capable models that do not use the
delta.reasoning_contentconvention. DeepSeek and Kimi both use it and are unaffected.
Suggested investigation
- Audit the OpenAI-compatible stream consumer to see how it splits
delta.contentwhen nodelta.reasoning_contentis present. Likely it should at minimum split on<think>...</think>boundaries for providers that use that syntax. - Audit the message-write path when
finish_reason: "stop"arrives with a non-emptycontentblob containing custom XML tags. Currently it appears to drop thetextpart while retaining areasoningpart — which is the inverse of the desired behaviour. - Consider treating any
<think>...</think>substring insidedelta.contentas a reasoning part regardless ofdelta.reasoning_contentpresence, since several providers (MiniMax, possibly Qwen via certain proxies) ship reasoning content inlined incontentrather than in a separate field. - Confirm whether this intersects with #42677 (ollama
gpt-oss:20b, qwen3), #42287 (session-title model returns only ``), and #42300 (v1 path drops variant body fields).
Workarounds (none are upstream fixes)
Until this is fixed, our options are:
- Switch the agent's primary model to a provider that emits
delta.reasoning_content(e.g.deepseek/deepseek-v4-flash,kimi-for-coding/k2p7). Confirmed working in the same session. - Write a thin local proxy that re-emits MiniMax-M3's SSE stream with reasoning split into
delta.reasoning_contentand answers intodelta.content, then point OpenCode'sbaseURLat the proxy. (We can provide a minimal Python implementation if useful.) - Disable
oh-my-openagent— does not fix this bug (we verified the same DB signature with and without OMO, because the issue is in the OpenAI-compatible provider layer, not the plugin).
Related issues
- #42677 — empty assistant stop message, zero parts, tokens counted (ollama / qwen3 / gpt-oss)
- #42287 — session title empty when title model returns only a `` block
- #42300 — v1 path drops variant body fields for
@ai-sdk/openai-compatible
Possibly also related to the oh-my-openagent model-capability work: PR #4719 (snapshot > heuristic for supportsThinking), PR #4716 (k2p* thinking fix), PR #3666 (mark minimax and non-thinking kimi as supportsThinking:false). Those are upstream-side fixes for a different symptom, but they confirm MiniMax-M3 is currently classified as thinking-capable in OMO 4.19+ bundled snapshots, so the request side is sending reasoning-capable requests and the model is returning reasoning — OpenCode just fails to materialize it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.