anomalyco / anomalyco/opencode

[Bug] MiniMax-M3 (reasoning model) silently finishes stop and produces no text/tool parts in OpenCode 1.18.18

Open
#43,029 2 comments 0 reactions 1 assignee View on GitHub

@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 marks reasoning: true)
  • Plugin: oh-my-openagent 4.19.0 (with PR #4719 fix already merged)
  • Agent: Sisyphus - Ultraworker
  • Reproducible: yes, on demand (see below)

Reproduction

  1. In OpenCode Web UI, open a session using Sisyphus - Ultraworker agent with default model minimax-cn-coding-plan/MiniMax-M3.
  2. 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.
  3. Observe the agent's first turn: it may emit one tool_call part correctly, then on the subsequent turn produce an assistant message with only reasoning content and no text or tool parts.
  4. The user prompt that reliably triggers it in our setup is "继续,把结论写出来" (continue, write the conclusion) after the agent has already found the relevant code.
  5. Switch the model dropdown to deepseek-v4-flash and send the same prompt → the same agent emits reasoning + text + tool parts 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:

  1. The provider sees only one big content string and never constructs a dedicated reasoning part — but does happen to attach a reasoning part (1741 chars in our example), suggesting some heuristic extraction may be running partially.
  2. The same content blob is not recognized as valid text because it contains markers and pseudo-tool-call tags, so OpenCode declines to materialize a text part.
  3. No native tool_calls are in the stream because MiniMax's pseudo-tool-call syntax is non-standard, so no tool part is created either.
  4. 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=627 in 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_content convention. DeepSeek and Kimi both use it and are unaffected.

Suggested investigation

  • Audit the OpenAI-compatible stream consumer to see how it splits delta.content when no delta.reasoning_content is 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-empty content blob containing custom XML tags. Currently it appears to drop the text part while retaining a reasoning part — which is the inverse of the desired behaviour.
  • Consider treating any <think>...</think> substring inside delta.content as a reasoning part regardless of delta.reasoning_content presence, since several providers (MiniMax, possibly Qwen via certain proxies) ship reasoning content inlined in content rather 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:

  1. 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.
  2. Write a thin local proxy that re-emits MiniMax-M3's SSE stream with reasoning split into delta.reasoning_content and answers into delta.content, then point OpenCode's baseURL at the proxy. (We can provide a minimal Python implementation if useful.)
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.