diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py
index b4e758218c5..72ea85dfa33 100644
--- a/litellm/litellm_core_utils/prompt_templates/common_utils.py
+++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py
@@ -1627,14 +1627,7 @@ def _parse_content_for_reasoning(
)
if reasoning_match:
- reasoning_content = reasoning_match.group(1)
- content = reasoning_match.group(2)
- if not content.strip():
- # Model's entire answer was inside the think block with
- # nothing after it — surface it as content instead of
- # silently discarding the model's only real output.
- content = reasoning_content
- return reasoning_content, content
+ return reasoning_match.group(1), reasoning_match.group(2)
return None, message_text
diff --git a/litellm/llms/minimax/chat/transformation.py b/litellm/llms/minimax/chat/transformation.py
index c5aa8811f02..5d6b4c35933 100644
--- a/litellm/llms/minimax/chat/transformation.py
+++ b/litellm/llms/minimax/chat/transformation.py
@@ -2,12 +2,20 @@
MiniMax OpenAI transformation config - extends OpenAI chat config for MiniMax's OpenAI-compatible API
"""
-from typing import Final
+from typing import TYPE_CHECKING, Any, Final
import litellm
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam
+from litellm.types.utils import ModelResponse
+
+if TYPE_CHECKING:
+ from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
+
+ LiteLLMLoggingObj = _LiteLLMLoggingObj
+else:
+ LiteLLMLoggingObj = Any
class MinimaxChatConfig(OpenAIGPTConfig):
@@ -96,3 +104,53 @@ class MinimaxChatConfig(OpenAIGPTConfig):
pass
return base_params + additional_params
+
+ def transform_response(
+ self,
+ model: str,
+ raw_response: httpx.Response,
+ model_response: ModelResponse,
+ logging_obj: LiteLLMLoggingObj,
+ request_data: dict,
+ messages: list[AllMessageValues],
+ optional_params: dict,
+ litellm_params: dict,
+ encoding,
+ api_key: str | None = None,
+ json_mode: bool | None = None,
+ ) -> ModelResponse:
+ """
+ MiniMax M2.7 (reasoning_split unset/false, the default) can return
+ its entire answer inside ... with nothing trailing
+ after the closing tag. The shared parser leaves `content` empty in
+ that case, discarding the model's only real output.
+
+ Scoped to MiniMax only: for other providers using tags,
+ content left empty after the tag is genuinely empty output, not a
+ signal to promote reasoning_content into the visible channel —
+ doing that generically risks leaking hidden reasoning for
+ adversarial prompts that end right after . MiniMax's docs
+ confirm the whole-answer-in- shape is expected behavior
+ for this provider specifically.
+ """
+ response = super().transform_response(
+ model=model,
+ raw_response=raw_response,
+ model_response=model_response,
+ logging_obj=logging_obj,
+ request_data=request_data,
+ messages=messages,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ encoding=encoding,
+ api_key=api_key,
+ json_mode=json_mode,
+ )
+ for choice in response.choices:
+ message = getattr(choice, "message", None)
+ if message is None:
+ continue
+ reasoning_content = getattr(message, "reasoning_content", None)
+ if reasoning_content and not (message.content or "").strip():
+ message.content = reasoning_content
+ return response
diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py
index ef21796655f..67f2e1ce06d 100644
--- a/tests/litellm_utils_tests/test_utils.py
+++ b/tests/litellm_utils_tests/test_utils.py
@@ -1043,15 +1043,6 @@ def test_convert_model_response_object():
"The sky is a canvas of blue",
),
("I am a regular response", None, "I am a regular response"),
- (
- # Regression for #38197: MiniMax M2.7 puts its entire answer
- # inside ... with nothing trailing after the
- # closing tag. Fall back to the reasoning content instead of
- # silently discarding the model's only real output.
- "The answer to 2+2 is 4.",
- "The answer to 2+2 is 4.",
- "The answer to 2+2 is 4.",
- ),
],
)
def test_parse_content_for_reasoning(content, expected_reasoning, expected_content):
diff --git a/tests/llm_translation/test_minimax_transformation.py b/tests/llm_translation/test_minimax_transformation.py
new file mode 100644
index 00000000000..ad3a0e2c94e
--- /dev/null
+++ b/tests/llm_translation/test_minimax_transformation.py
@@ -0,0 +1,87 @@
+"""
+Regression test for #38197: MiniMax M2.7 can return its entire answer
+inside ... with nothing trailing after the closing tag.
+MinimaxChatConfig.transform_response() should fall back to
+reasoning_content in that case instead of leaving content empty.
+
+Scoped to MiniMax only — see the docstring on transform_response for why
+this isn't in the shared _parse_content_for_reasoning function.
+"""
+from unittest.mock import MagicMock
+
+from litellm.llms.minimax.chat.transformation import MinimaxChatConfig
+
+
+class TestMinimaxTransformResponse:
+ def test_empty_content_falls_back_to_reasoning_content(self):
+ config = MinimaxChatConfig()
+
+ fake_message = MagicMock()
+ fake_message.content = ""
+ fake_message.reasoning_content = "The answer to 2+2 is 4."
+
+ fake_choice = MagicMock()
+ fake_choice.message = fake_message
+
+ fake_model_response = MagicMock()
+ fake_model_response.choices = [fake_choice]
+
+ import litellm.llms.openai.chat.gpt_transformation as parent_module
+
+ original = parent_module.OpenAIGPTConfig.transform_response
+ parent_module.OpenAIGPTConfig.transform_response = (
+ lambda self, **kwargs: fake_model_response
+ )
+
+ try:
+ result = config.transform_response(
+ model="minimax/MiniMax-M2.7",
+ raw_response=None,
+ model_response=fake_model_response,
+ logging_obj=None,
+ request_data={},
+ messages=[],
+ optional_params={},
+ litellm_params={},
+ encoding=None,
+ )
+ assert result.choices[0].message.content == "The answer to 2+2 is 4."
+ finally:
+ parent_module.OpenAIGPTConfig.transform_response = original
+
+ def test_normal_content_left_untouched(self):
+ """Sanity check: when content is already populated, don't overwrite it."""
+ config = MinimaxChatConfig()
+
+ fake_message = MagicMock()
+ fake_message.content = "The answer is 4."
+ fake_message.reasoning_content = "Let me think about this."
+
+ fake_choice = MagicMock()
+ fake_choice.message = fake_message
+
+ fake_model_response = MagicMock()
+ fake_model_response.choices = [fake_choice]
+
+ import litellm.llms.openai.chat.gpt_transformation as parent_module
+
+ original = parent_module.OpenAIGPTConfig.transform_response
+ parent_module.OpenAIGPTConfig.transform_response = (
+ lambda self, **kwargs: fake_model_response
+ )
+
+ try:
+ result = config.transform_response(
+ model="minimax/MiniMax-M2.7",
+ raw_response=None,
+ model_response=fake_model_response,
+ logging_obj=None,
+ request_data={},
+ messages=[],
+ optional_params={},
+ litellm_params={},
+ encoding=None,
+ )
+ assert result.choices[0].message.content == "The answer is 4."
+ finally:
+ parent_module.OpenAIGPTConfig.transform_response = original