diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index a3a13d6d6..f29d90ff9 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -75,6 +75,22 @@ class TestMinimaxExactMatches: def test_m2_base_rejects_tool_choice(self): assert get_capabilities("MiniMax-M2").supports_tool_choice is False + def test_m2_x_requires_reasoning_split(self): + # M2.x reasoning models need reasoning_split=True so blocks + # land in reasoning_details instead of content (#826). + for model in ("MiniMax-M2.7", "MiniMax-M2.5-highspeed", "MiniMax-M2"): + assert get_capabilities(model).requires_reasoning_split is True + + def test_future_m3_inherits_reasoning_split(self): + assert get_capabilities("MiniMax-M3-highspeed").requires_reasoning_split is True + + def test_non_reasoning_minimax_does_not_get_reasoning_split(self): + # Coding Plan, MiniMax-Text-01, and any non-M2-prefixed MiniMax model + # reject the reasoning_split kwarg via the openai SDK's strict + # validation (#826). Default capability has it disabled. + for model in ("minimax-text-01", "MiniMax-Coding-Plan", "abab6.5-chat"): + assert get_capabilities(model).requires_reasoning_split is False + @pytest.mark.unit class TestDefault: diff --git a/tests/test_minimax.py b/tests/test_minimax.py index c48735429..f33d2e7b7 100644 --- a/tests/test_minimax.py +++ b/tests/test_minimax.py @@ -42,6 +42,18 @@ class TestMinimaxReasoningSplit: # the caller passed. setdefault leaves an existing value alone. assert payload.get("reasoning_split") in (False, True) + def test_non_reasoning_minimax_does_not_inject_reasoning_split(self): + """Coding Plan / MiniMax-Text-01 / any non-M2-prefixed model must NOT + receive reasoning_split — the openai SDK rejects unknown kwargs with + TypeError (#826).""" + for model in ("minimax-text-01", "MiniMax-Coding-Plan"): + payload = _client(model)._get_request_payload( + [HumanMessage(content="hi")] + ) + assert "reasoning_split" not in payload, ( + f"{model!r} payload unexpectedly contains reasoning_split" + ) + @pytest.mark.unit class TestMinimaxStructuredOutputDispatch: diff --git a/tradingagents/llm_clients/capabilities.py b/tradingagents/llm_clients/capabilities.py index d8e21175c..8c95d2641 100644 --- a/tradingagents/llm_clients/capabilities.py +++ b/tradingagents/llm_clients/capabilities.py @@ -38,6 +38,12 @@ class ModelCapabilities: # DeepSeek thinking-mode models 400 if reasoning_content from prior # assistant turns is not echoed back on the next request. requires_reasoning_content_roundtrip: bool = False + # MiniMax M2.x reasoning models need ``reasoning_split=True`` so the + # block lands in ``reasoning_details`` instead of polluting + # ``content``. The flag is rejected by non-reasoning MiniMax models + # (Coding Plan, MiniMax-Text-01, etc.), so we only set it where the + # model actually consumes it. (#826) + requires_reasoning_split: bool = False # DeepSeek's thinking models accept the ``tools`` array but reject the @@ -74,6 +80,7 @@ _MINIMAX_THINKING = ModelCapabilities( supports_json_mode=False, supports_json_schema=False, preferred_structured_method="function_calling", + requires_reasoning_split=True, ) _DEFAULT = ModelCapabilities( diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 42db9677f..b4f94c6f4 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -118,6 +118,11 @@ class MinimaxChatOpenAI(NormalizedChatOpenAI): ``reasoning_split=True`` in the request body redirects the thinking block into ``reasoning_details`` so ``content`` stays clean. + The flag is gated by ``ModelCapabilities.requires_reasoning_split`` + because non-reasoning MiniMax endpoints (Coding Plan, MiniMax-Text-01) + reject the parameter via the openai SDK's strict kwarg validation + (#826). + Tool-choice handling for M2.x — those models accept only the string enum ``{"none", "auto"}`` and reject langchain's function-spec dict — is handled by the capability dispatch in @@ -126,7 +131,8 @@ class MinimaxChatOpenAI(NormalizedChatOpenAI): def _get_request_payload(self, input_, *, stop=None, **kwargs): payload = super()._get_request_payload(input_, stop=stop, **kwargs) - payload.setdefault("reasoning_split", True) + if get_capabilities(self.model_name).requires_reasoning_split: + payload.setdefault("reasoning_split", True) return payload