From 1b15772c86e45e0524dae7cb96ca25e2aaa7ffce Mon Sep 17 00:00:00 2001 From: trakshan-mishra Date: Tue, 25 Aug 2026 19:43:52 +0530 Subject: [PATCH 1/6] fix(reasoning): fall back to reasoning_content when nothing follows tag Fixes #38197. When a reasoning model's entire answer sits inside ... with nothing trailing after the closing tag, _parse_content_for_reasoning() previously returned an empty string as content, discarding the model's only real output even though finish_reason=stop and the call succeeded. --- .../prompt_templates/common_utils.py | 9 +++- .../test_minimax_reasoning_content_bug.py | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/minimax_bugfix/test_minimax_reasoning_content_bug.py diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 72ea85dfa33..b4e758218c5 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -1627,7 +1627,14 @@ def _parse_content_for_reasoning( ) if reasoning_match: - return reasoning_match.group(1), reasoning_match.group(2) + 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 None, message_text diff --git a/tests/minimax_bugfix/test_minimax_reasoning_content_bug.py b/tests/minimax_bugfix/test_minimax_reasoning_content_bug.py new file mode 100644 index 00000000000..fad8a3d1ce5 --- /dev/null +++ b/tests/minimax_bugfix/test_minimax_reasoning_content_bug.py @@ -0,0 +1,43 @@ +""" +Repro for GitHub issue #38197: MiniMax-M2.7 returns message.content empty, +usage all zero, while reasoning_content has valid output, finish_reason=stop. + +Root cause (confirmed against source + MiniMax docs, 2026-08-25): +MiniMax M2.7 (with reasoning_split unset/false, the default) returns its +answer wrapped as "..." in a single content +string. litellm.litellm_core_utils.prompt_templates.common_utils. +_parse_content_for_reasoning() splits this with a regex whose second capture +group is everything AFTER the closing tag. When the model's real +answer sits entirely inside the block with nothing after it, that +capture group is an empty string — so `content` comes back "" while the +actual answer is sitting, discarded, in `reasoning_content`. + +Tests the parsing function directly — no network, no API key, no mock +server needed, since this isolates exactly where the bug lives. +""" +from litellm.litellm_core_utils.prompt_templates.common_utils import ( + _parse_content_for_reasoning, +) + + +class TestMinimaxReasoningContentBug: + def test_answer_entirely_inside_think_tag_yields_empty_content(self): + raw = "The answer to 2+2 is 4." + + reasoning_content, content = _parse_content_for_reasoning(raw) + + print("reasoning_content:", repr(reasoning_content)) + print("content:", repr(content)) + + assert reasoning_content == "The answer to 2+2 is 4." + # Fixed: content now falls back to reasoning_content when nothing + # follows the closing tag, instead of being empty. + assert content == "The answer to 2+2 is 4." + + def test_answer_after_think_tag_still_works(self): + raw = "Let me work this out.The answer is 4." + + reasoning_content, content = _parse_content_for_reasoning(raw) + + assert reasoning_content == "Let me work this out." + assert content == "The answer is 4." From 577e94847daef8d5d4b11a6a48cd21f846fe4ef2 Mon Sep 17 00:00:00 2001 From: trakshan-mishra Date: Tue, 25 Aug 2026 19:56:49 +0530 Subject: [PATCH 2/6] test: merge minimax reasoning_content regression case into existing test_parse_content_for_reasoning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses reviewer feedback — consolidates coverage into the existing parametrized test instead of a standalone module. --- tests/litellm_utils_tests/test_utils.py | 9 ++++ .../test_minimax_reasoning_content_bug.py | 43 ------------------- 2 files changed, 9 insertions(+), 43 deletions(-) delete mode 100644 tests/minimax_bugfix/test_minimax_reasoning_content_bug.py diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index 67f2e1ce06d..ef21796655f 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -1043,6 +1043,15 @@ 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/minimax_bugfix/test_minimax_reasoning_content_bug.py b/tests/minimax_bugfix/test_minimax_reasoning_content_bug.py deleted file mode 100644 index fad8a3d1ce5..00000000000 --- a/tests/minimax_bugfix/test_minimax_reasoning_content_bug.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Repro for GitHub issue #38197: MiniMax-M2.7 returns message.content empty, -usage all zero, while reasoning_content has valid output, finish_reason=stop. - -Root cause (confirmed against source + MiniMax docs, 2026-08-25): -MiniMax M2.7 (with reasoning_split unset/false, the default) returns its -answer wrapped as "..." in a single content -string. litellm.litellm_core_utils.prompt_templates.common_utils. -_parse_content_for_reasoning() splits this with a regex whose second capture -group is everything AFTER the closing tag. When the model's real -answer sits entirely inside the block with nothing after it, that -capture group is an empty string — so `content` comes back "" while the -actual answer is sitting, discarded, in `reasoning_content`. - -Tests the parsing function directly — no network, no API key, no mock -server needed, since this isolates exactly where the bug lives. -""" -from litellm.litellm_core_utils.prompt_templates.common_utils import ( - _parse_content_for_reasoning, -) - - -class TestMinimaxReasoningContentBug: - def test_answer_entirely_inside_think_tag_yields_empty_content(self): - raw = "The answer to 2+2 is 4." - - reasoning_content, content = _parse_content_for_reasoning(raw) - - print("reasoning_content:", repr(reasoning_content)) - print("content:", repr(content)) - - assert reasoning_content == "The answer to 2+2 is 4." - # Fixed: content now falls back to reasoning_content when nothing - # follows the closing tag, instead of being empty. - assert content == "The answer to 2+2 is 4." - - def test_answer_after_think_tag_still_works(self): - raw = "Let me work this out.The answer is 4." - - reasoning_content, content = _parse_content_for_reasoning(raw) - - assert reasoning_content == "Let me work this out." - assert content == "The answer is 4." From ad421e03d20fcd96f79a120658e2e651d88dfb34 Mon Sep 17 00:00:00 2001 From: trakshan-mishra Date: Tue, 25 Aug 2026 20:07:06 +0530 Subject: [PATCH 3/6] fix(minimax): scope reasoning_content fallback to MiniMax only, not shared parser Addresses security review feedback: the earlier fix lived in the shared _parse_content_for_reasoning() function, which is used by every provider that emits tags (OpenAI-compatible, Bedrock, Ollama). Promoting empty content to reasoning_content there risks exposing hidden reasoning (which may include system instructions or sensitive context) for any provider where an adversarial prompt can end generation right after . Moved the fallback into MinimaxChatConfig.transform_response() instead. MiniMax's own docs confirm the whole-answer-in- shape is expected behavior specifically for this provider when reasoning_split is unset, so the fallback is safe and correct only in this scope. - Reverted the shared-function change entirely - Added MinimaxChatConfig.transform_response() override - New tests in tests/llm_translation/test_minimax_transformation.py cover both the fallback case and the already-correct pass-through case --- .../prompt_templates/common_utils.py | 9 +- litellm/llms/minimax/chat/transformation.py | 60 ++++++++++++- tests/litellm_utils_tests/test_utils.py | 9 -- .../test_minimax_transformation.py | 87 +++++++++++++++++++ 4 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 tests/llm_translation/test_minimax_transformation.py 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 From 2f767a413667b5594d19b03572b6b1330478493f Mon Sep 17 00:00:00 2001 From: trakshan-mishra Date: Tue, 25 Aug 2026 20:29:44 +0530 Subject: [PATCH 4/6] fix: add missing httpx import in minimax transformation.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruff caught this as F821 (undefined name httpx) — the transform_response override's raw_response parameter is typed as httpx.Response, but the module never imported httpx. Import order fixed via ruff --fix. --- litellm/llms/minimax/chat/transformation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/litellm/llms/minimax/chat/transformation.py b/litellm/llms/minimax/chat/transformation.py index 5d6b4c35933..1f7943fbc41 100644 --- a/litellm/llms/minimax/chat/transformation.py +++ b/litellm/llms/minimax/chat/transformation.py @@ -4,6 +4,8 @@ MiniMax OpenAI transformation config - extends OpenAI chat config for MiniMax's from typing import TYPE_CHECKING, Any, Final +import httpx + import litellm from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig from litellm.secret_managers.main import get_secret_str From dfdd227d5b4ebedf2361138bb7d2abf8a24b4cb8 Mon Sep 17 00:00:00 2001 From: trakshan-mishra Date: Tue, 25 Aug 2026 21:21:31 +0530 Subject: [PATCH 5/6] fix(minimax): suppress TID251 on Any alias and cover transform_response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on #38212 was failing two required checks: 1. ruff strict gate — TID251 over budget (1215 > base 1214). The new transform_response override imported typing.Any, pushing the total one over the base and getting this PR blamed. Suppressed with a reasoned noqa (the documented escape hatch per ruff-strict.toml:47), consistent with the two existing TID251 suppressions in the repo. 2. codecov/patch — 35.71% < 69.75% target. The new override was untested; the PR's earlier test only covered the shared parser. Added 3 unit tests covering the override's promotion path: - promotes reasoning_content when content is empty (the #38197 bug) - keeps content when already present (no clobber) - no-op when reasoning_content is absent File coverage on the override is now 76%. --- litellm/llms/minimax/chat/transformation.py | 6 +- .../llms/minimax/chat/test_transformation.py | 103 +++++++++++++++++- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/litellm/llms/minimax/chat/transformation.py b/litellm/llms/minimax/chat/transformation.py index 1f7943fbc41..2c0e663b848 100644 --- a/litellm/llms/minimax/chat/transformation.py +++ b/litellm/llms/minimax/chat/transformation.py @@ -2,7 +2,11 @@ MiniMax OpenAI transformation config - extends OpenAI chat config for MiniMax's OpenAI-compatible API """ -from typing import TYPE_CHECKING, Any, Final +from typing import ( + TYPE_CHECKING, + Any, # noqa: TID251 # LiteLLMLoggingObj has no concrete public type; matches OpenAIGPTConfig's own alias + Final, +) import httpx diff --git a/tests/test_litellm/llms/minimax/chat/test_transformation.py b/tests/test_litellm/llms/minimax/chat/test_transformation.py index 9d51b556500..7401850133e 100644 --- a/tests/test_litellm/llms/minimax/chat/test_transformation.py +++ b/tests/test_litellm/llms/minimax/chat/test_transformation.py @@ -11,6 +11,7 @@ import pytest import litellm from litellm import completion from litellm.llms.minimax.chat.transformation import MinimaxChatConfig +from litellm.types.utils import Choices, Message, ModelResponse def test_minimax_chat_config(): @@ -99,14 +100,110 @@ def test_minimax_provider_config_manager(): from litellm.types.utils import LlmProviders from litellm.utils import ProviderConfigManager - config = ProviderConfigManager.get_provider_chat_config( - model="MiniMax-M2.1", provider=LlmProviders.MINIMAX - ) + config = ProviderConfigManager.get_provider_chat_config(model="MiniMax-M2.1", provider=LlmProviders.MINIMAX) assert config is not None assert isinstance(config, MinimaxChatConfig) +def _build_response_with_reasoning(content: str | None, reasoning_content: str | None): + """Helper: a ModelResponse whose single choice has the given content/reasoning_content.""" + message = Message(content=content, role="assistant", reasoning_content=reasoning_content) + return ModelResponse( + id="test", + choices=[Choices(finish_reason="stop", index=0, message=message)], + model="MiniMax-M2.1", + ) + + +def test_transform_response_promotes_reasoning_content_when_content_empty(): + """Issue #38197: when the model's whole answer sits inside + with nothing trailing, the shared parser leaves content empty. The override + must fall back to reasoning_content so the model's output isn't discarded.""" + config = MinimaxChatConfig() + raw = MagicMock(status_code=200, json=lambda: {}) + original = _build_response_with_reasoning( + content=None, + reasoning_content="The answer to 2+2 is 4.", + ) + + with patch( + "litellm.llms.openai.chat.gpt_transformation.OpenAIGPTConfig.transform_response", + return_value=original, + ): + result = config.transform_response( + model="MiniMax-M2.1", + raw_response=raw, + model_response=ModelResponse(model="MiniMax-M2.1"), + logging_obj=MagicMock(), + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].message.content == "The answer to 2+2 is 4." + + +def test_transform_response_keeps_content_when_already_present(): + """When content is non-empty (answer follows the tag), the override + must not clobber it with reasoning_content.""" + config = MinimaxChatConfig() + raw = MagicMock(status_code=200, json=lambda: {}) + original = _build_response_with_reasoning( + content="The answer is 4.", + reasoning_content="Let me work this out.", + ) + + with patch( + "litellm.llms.openai.chat.gpt_transformation.OpenAIGPTConfig.transform_response", + return_value=original, + ): + result = config.transform_response( + model="MiniMax-M2.1", + raw_response=raw, + model_response=ModelResponse(model="MiniMax-M2.1"), + logging_obj=MagicMock(), + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].message.content == "The answer is 4." + assert result.choices[0].message.reasoning_content == "Let me work this out." + + +def test_transform_response_noop_without_reasoning_content(): + """When reasoning_content is absent/None, content is left untouched.""" + config = MinimaxChatConfig() + raw = MagicMock(status_code=200, json=lambda: {}) + original = _build_response_with_reasoning( + content="plain answer", + reasoning_content=None, + ) + + with patch( + "litellm.llms.openai.chat.gpt_transformation.OpenAIGPTConfig.transform_response", + return_value=original, + ): + result = config.transform_response( + model="MiniMax-M2.1", + raw_response=raw, + model_response=ModelResponse(model="MiniMax-M2.1"), + logging_obj=MagicMock(), + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].message.content == "plain answer" + + @pytest.mark.skip(reason="Requires actual MiniMax API key") def test_minimax_chat_completion_basic(): """Test basic chat completion with MiniMax OpenAI-compatible API""" From a0d5a8506a3280ae61e383715780018d9c383c55 Mon Sep 17 00:00:00 2001 From: trakshan-mishra Date: Tue, 25 Aug 2026 21:58:22 +0530 Subject: [PATCH 6/6] fix(minimax): suppress LIT001/TID251/TQ008 and cover transform_response Resolves CI lint + codecov failures on #38212: - TID251: suppress Any import (matches OpenAIGPTConfig's alias) - LIT001: suppress mutable dict params (signature must match parent) - TQ008: suppress patch() of SDK internal (isolates override logic) - basedpyright: pyright:ignore on params/super() call (matches parent) - Codecov: add 3 unit tests covering reasoning_content fallback path --- litellm/llms/minimax/chat/transformation.py | 18 ++++++++---------- .../llms/minimax/chat/test_transformation.py | 6 +++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/litellm/llms/minimax/chat/transformation.py b/litellm/llms/minimax/chat/transformation.py index 2c0e663b848..9b651bef699 100644 --- a/litellm/llms/minimax/chat/transformation.py +++ b/litellm/llms/minimax/chat/transformation.py @@ -117,11 +117,11 @@ class MinimaxChatConfig(OpenAIGPTConfig): raw_response: httpx.Response, model_response: ModelResponse, logging_obj: LiteLLMLoggingObj, - request_data: dict, - messages: list[AllMessageValues], - optional_params: dict, - litellm_params: dict, - encoding, + request_data: dict, # mutable-ok: matches parent # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] # matches OpenAIGPTConfig.transform_response + messages: list[AllMessageValues], # mutable-ok: matches parent + optional_params: dict, # mutable-ok: matches parent # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] # matches OpenAIGPTConfig.transform_response + litellm_params: dict, # mutable-ok: matches parent # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] # matches OpenAIGPTConfig.transform_response + encoding, # pyright: ignore[reportAny,reportMissingParameterType] # matches OpenAIGPTConfig.transform_response api_key: str | None = None, json_mode: bool | None = None, ) -> ModelResponse: @@ -139,7 +139,7 @@ class MinimaxChatConfig(OpenAIGPTConfig): confirm the whole-answer-in- shape is expected behavior for this provider specifically. """ - response = super().transform_response( + response = super().transform_response( # pyright: ignore[reportUnknownMemberType] # super() inherits partially unknown param types from parent model=model, raw_response=raw_response, model_response=model_response, @@ -153,10 +153,8 @@ class MinimaxChatConfig(OpenAIGPTConfig): 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) + message = choice.message + reasoning_content = getattr(message, "reasoning_content", None) # pyright: ignore[reportAny] # Message deletes reasoning_content when None if reasoning_content and not (message.content or "").strip(): message.content = reasoning_content return response diff --git a/tests/test_litellm/llms/minimax/chat/test_transformation.py b/tests/test_litellm/llms/minimax/chat/test_transformation.py index 7401850133e..f5488fefd58 100644 --- a/tests/test_litellm/llms/minimax/chat/test_transformation.py +++ b/tests/test_litellm/llms/minimax/chat/test_transformation.py @@ -127,7 +127,7 @@ def test_transform_response_promotes_reasoning_content_when_content_empty(): reasoning_content="The answer to 2+2 is 4.", ) - with patch( + with patch( # test-quality-ok: isolates override's reasoning_content fallback from parent's HTTP/parsing machinery; no injection seam for super().transform_response "litellm.llms.openai.chat.gpt_transformation.OpenAIGPTConfig.transform_response", return_value=original, ): @@ -156,7 +156,7 @@ def test_transform_response_keeps_content_when_already_present(): reasoning_content="Let me work this out.", ) - with patch( + with patch( # test-quality-ok: isolates override's no-clobber path from parent's HTTP/parsing machinery; no injection seam for super().transform_response "litellm.llms.openai.chat.gpt_transformation.OpenAIGPTConfig.transform_response", return_value=original, ): @@ -185,7 +185,7 @@ def test_transform_response_noop_without_reasoning_content(): reasoning_content=None, ) - with patch( + with patch( # test-quality-ok: isolates override's no-op path from parent's HTTP/parsing machinery; no injection seam for super().transform_response "litellm.llms.openai.chat.gpt_transformation.OpenAIGPTConfig.transform_response", return_value=original, ):