diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 691d796273f7..ea0a02917eba 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -611,7 +611,8 @@ def strip_think_blocks(agent, content: str) -> str:
```` in prose aren't over-stripped.
3. Stray orphan open/close tags that slip through.
4. Tag variants: ````, ````, ````,
- ````, ```` (Gemma 4), all
+ ````, ```` (Gemma 4),
+ ```` (MiniMax M3), all
case-insensitive.
Additionally strips standalone tool-call XML blocks that some open
@@ -638,6 +639,7 @@ def strip_think_blocks(agent, content: str) -> str:
content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE)
content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE)
content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE)
+ content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE)
# 1b. Tool-call XML blocks (openclaw/openclaw#67318). Handle the
# generic tag names first — they have no attribute gating since
# a literal in prose is already vanishingly rare.
@@ -667,14 +669,14 @@ def strip_think_blocks(agent, content: str) -> str:
# Strip from the tag to end of string. Fixes #8878 / #9568
# (MiniMax M2.7 leaking raw reasoning into assistant content).
content = re.sub(
- r'(?:^|\n)[ \t]*<(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD)\b[^>]*>.*$',
+ r'(?:^|\n)[ \t]*<(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD|mm:think)\b[^>]*>.*$',
'',
content,
flags=re.DOTALL | re.IGNORECASE,
)
# 3. Stray orphan open/close tags that slipped through.
content = re.sub(
- r'?(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD)>\s*',
+ r'?(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD|mm:think)>\s*',
'',
content,
flags=re.IGNORECASE,
diff --git a/cli.py b/cli.py
index 4dc546bac4e8..ae8e79584879 100644
--- a/cli.py
+++ b/cli.py
@@ -188,6 +188,7 @@ def realign_markdown_tables(*args, **kwargs):
"thinking",
"reasoning",
"thought",
+ "mm:think",
)
@@ -202,8 +203,8 @@ def _strip_reasoning_tags(text: str) -> str:
partial-content dumps.
Covers the variants emitted by reasoning models today: ````,
- ````, ````, ````, and
- ```` (Gemma 4). Must stay in sync with
+ ````, ````, ````,
+ ```` (Gemma 4), and ```` (MiniMax M3). Must stay in sync with
``run_agent.py::_strip_think_blocks`` and the stream consumer's
``_OPEN_THINK_TAGS`` / ``_CLOSE_THINK_TAGS`` tuples.
@@ -5628,8 +5629,8 @@ def _stream_delta(self, text) -> None:
# suppress them during streaming too — unless show_reasoning is
# enabled, in which case we route the inner content to the
# reasoning display box instead of discarding it.
- _OPEN_TAGS = ("", "", "", "", "", "")
- _CLOSE_TAGS = ("", "", "", "", "", "")
+ _OPEN_TAGS = ("", "", "", "", "", "", "")
+ _CLOSE_TAGS = ("", "", "", "", "", "", "")
# Append to a pre-filter buffer first
self._stream_prefilt = getattr(self, "_stream_prefilt", "") + text
diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py
index a08e169f2f99..fdef157f538c 100644
--- a/gateway/stream_consumer.py
+++ b/gateway/stream_consumer.py
@@ -104,11 +104,11 @@ class GatewayStreamConsumer:
# run_agent.py _strip_think_blocks() tag variants.
_OPEN_THINK_TAGS = (
"", "", "",
- "", "", "",
+ "", "", "", "",
)
_CLOSE_THINK_TAGS = (
"", "", "",
- "", "", "",
+ "", "", "", "",
)
# Class-wide monotonic counter for native-streaming draft ids. Telegram
diff --git a/tests/cli/test_stream_delta_think_tag.py b/tests/cli/test_stream_delta_think_tag.py
index 331988bfab1d..ac8ccb703496 100644
--- a/tests/cli/test_stream_delta_think_tag.py
+++ b/tests/cli/test_stream_delta_think_tag.py
@@ -125,6 +125,15 @@ def test_reasoning_tags_are_case_insensitive(self, tag):
assert full == "Visible answer"
assert "hidden reasoning" not in full
+ def test_minimax_mm_think_tag_suppressed(self):
+ """MiniMax M3's channel marker should not reach the TUI."""
+ cli = _make_cli_stub()
+ cli._stream_delta("provider reasoningVisible answer")
+ assert not cli._in_reasoning_block
+ full = "".join(cli._emitted)
+ assert full == "Visible answer"
+ assert "provider reasoning" not in full
+
class TestFlushRecovery:
"""_flush_stream should recover content from false-positive reasoning blocks."""
diff --git a/tests/gateway/test_stream_consumer.py b/tests/gateway/test_stream_consumer.py
index cd49d3d74782..89382e2d5272 100644
--- a/tests/gateway/test_stream_consumer.py
+++ b/tests/gateway/test_stream_consumer.py
@@ -1452,6 +1452,13 @@ def test_thought_tag_variant(self):
c._filter_and_accumulate("Gemma styleOutput")
assert c._accumulated == "Output"
+ def test_minimax_mm_think_tag_variant(self):
+ """MiniMax M3 emits blocks that must stay hidden."""
+ c = _make_consumer()
+ c._filter_and_accumulate("provider reasoningAnswer")
+ assert c._accumulated == "Answer"
+ assert "provider reasoning" not in c._accumulated
+
def test_reasoning_scratchpad_variant(self):
c = _make_consumer()
c._filter_and_accumulate(
diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py
index da8a446bf8d8..25e28b16335d 100644
--- a/tests/run_agent/test_run_agent.py
+++ b/tests/run_agent/test_run_agent.py
@@ -392,6 +392,15 @@ def test_thought_block_removed(self, agent):
assert "" not in result
assert "answer" in result
+ def test_minimax_mm_think_block_removed(self, agent):
+ """MiniMax M3 uses tags for provider-side reasoning."""
+ result = agent._strip_think_blocks(
+ "internal reasoning answer"
+ )
+ assert "internal reasoning" not in result
+ assert "" not in result
+ assert "answer" in result
+
def test_orphaned_thought_tag(self, agent):
result = agent._strip_think_blocks("orphaned reasoning without close")
assert "" not in result