diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index 49bc91f44d2..c0b2a13d250 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -531,6 +531,7 @@ class ChatCompletionsTransport(ProviderTransport): supports_reasoning=params.get("supports_reasoning", False), qwen_session_metadata=params.get("qwen_session_metadata"), model=model, + base_url=params.get("base_url"), ollama_num_ctx=params.get("ollama_num_ctx"), session_id=params.get("session_id"), ) diff --git a/plugins/model-providers/minimax/__init__.py b/plugins/model-providers/minimax/__init__.py index 6d77536acee..7dbaf4000c2 100644 --- a/plugins/model-providers/minimax/__init__.py +++ b/plugins/model-providers/minimax/__init__.py @@ -1,13 +1,65 @@ """MiniMax provider profiles (international + China). -Both use anthropic_messages api_mode — their inference_base_url -ends with /anthropic which triggers auto-detection to anthropic_messages. +The default API-key routes use anthropic_messages because their base URLs end +with /anthropic. Users can opt MiniMax-M3 into the OpenAI-compatible endpoint +with base_url=https://api.minimax.io/v1; that route needs MiniMax-specific +reasoning controls in extra_body. """ +from typing import Any +from urllib.parse import urlparse + from providers import register_provider from providers.base import ProviderProfile -minimax = ProviderProfile( + +def _is_minimax_global_openai_base_url(base_url: str | None) -> bool: + parsed = urlparse(str(base_url or "").strip()) + if (parsed.hostname or "").lower() != "api.minimax.io": + return False + path = parsed.path.rstrip("/").lower() + return path == "/v1" + + +def _is_minimax_m3(model: str | None) -> bool: + normalized = str(model or "").strip().lower() + return normalized in {"minimax-m3", "minimax/minimax-m3"} + + +class MiniMaxProfile(ProviderProfile): + """MiniMax — M3 OpenAI-compatible reasoning controls.""" + + def build_api_kwargs_extras( + self, + *, + reasoning_config: dict | None = None, + model: str | None = None, + base_url: str | None = None, + **context: Any, + ) -> tuple[dict[str, Any], dict[str, Any]]: + """Emit M3 reasoning controls for api.minimax.io/v1. + + MiniMax-M3's OpenAI-compatible endpoint keeps thinking inline unless + ``reasoning_split`` is sent, so always request the split format on that + route. ``thinking`` controls the M3 mode; Hermes' effort levels are not + a MiniMax depth knob here, so they only select adaptive vs disabled. + """ + if not _is_minimax_global_openai_base_url(base_url) or not _is_minimax_m3(model): + return {}, {} + + extra_body: dict[str, Any] = {"reasoning_split": True} + + if isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is False: + extra_body["thinking"] = {"type": "disabled"} + return extra_body, {} + + if reasoning_config is not None: + extra_body["thinking"] = {"type": "adaptive"} + + return extra_body, {} + + +minimax = MiniMaxProfile( name="minimax", aliases=("mini-max",), api_mode="anthropic_messages", @@ -17,7 +69,7 @@ minimax = ProviderProfile( default_aux_model="MiniMax-M3", ) -minimax_cn = ProviderProfile( +minimax_cn = MiniMaxProfile( name="minimax-cn", aliases=("minimax-china", "minimax_cn"), api_mode="anthropic_messages", @@ -27,7 +79,7 @@ minimax_cn = ProviderProfile( default_aux_model="MiniMax-M3", ) -minimax_oauth = ProviderProfile( +minimax_oauth = MiniMaxProfile( name="minimax-oauth", aliases=("minimax_oauth", "minimax-oauth-io"), api_mode="anthropic_messages", diff --git a/run_agent.py b/run_agent.py index 8568c3eb2a4..2bf27d57510 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4792,15 +4792,6 @@ class AIAgent: return bool(github_model_reasoning_efforts(self.model)) except Exception: return False - if base_url_host_matches(self._base_url_lower, "api.minimax.io"): - # MiniMax (api.minimax.io): enable reasoning extra_body - # (reasoning_split, thinking, reasoning_effort) for both the - # Anthropic-format and OpenAI-format endpoints. Without this the - # safety gate strips those fields before they reach the API, so M3 - # leaks thinking into response content and burns output tokens. M3 - # specifically benefits from the OpenAI-compatible endpoint - # (/v1/chat/completions) for prompt caching. - return True if (self.provider or "").strip().lower() == "lmstudio": opts = self._lmstudio_reasoning_options_cached() # "off-only" (or absent) means no real reasoning capability. diff --git a/scripts/release.py b/scripts/release.py index e5b0fd2226d..4ca38841224 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1318,6 +1318,7 @@ AUTHOR_MAP = { "agentsmithlaor@gmail.com": "oferlaor", # PR #22356 salvage (cron origin sender identity) "jhin.lee@unity3d.com": "leehack", # PR #22053 salvage (telegram DM topic reply fallback) "caojiguang@gmail.com": "caojiguang", # PR #35117 carries #31853 (weixin _api_post/_api_get wait_for) + "gooku94123@gmail.com": "goku94123", # PR #46609 salvage (MiniMax reasoning extra_body) # pander: empty email, salvaged via PR #19665 from #16126 by @ms-alan "ayman.a.kamal@hotmail.com": "A-kamal", # PR #18678 (xAI image resolution fix) # Kanban bug-fix batch salvage (May 2026) diff --git a/tests/plugins/model_providers/test_minimax_profile.py b/tests/plugins/model_providers/test_minimax_profile.py index e66b0dea8ba..58c7b4fbe3f 100644 --- a/tests/plugins/model_providers/test_minimax_profile.py +++ b/tests/plugins/model_providers/test_minimax_profile.py @@ -118,3 +118,115 @@ class TestMinimaxAuxModelNotHighspeed: "is a -highspeed variant — that costs 2x for the same model and " "broke #4082 the first time. Revert to plain M2.7 or M3." ) + + +class TestMinimaxM3OpenAIReasoningWireShape: + """MiniMax-M3 on api.minimax.io/v1 gets MiniMax's OpenAI-compatible knobs.""" + + def test_m3_openai_route_requests_reasoning_split_by_default(self): + import model_tools # noqa: F401 + import providers + + profile = providers.get_provider_profile("minimax") + assert profile is not None + extra_body, top_level = profile.build_api_kwargs_extras( + reasoning_config=None, + model="MiniMax-M3", + base_url="https://api.minimax.io/v1", + ) + assert extra_body == {"reasoning_split": True} + assert top_level == {} + + def test_m3_openai_route_maps_explicit_effort_to_adaptive_only(self): + import model_tools # noqa: F401 + import providers + + profile = providers.get_provider_profile("minimax") + assert profile is not None + extra_body, top_level = profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "high"}, + model="MiniMax-M3", + base_url="https://api.minimax.io/v1", + ) + assert extra_body == { + "reasoning_split": True, + "thinking": {"type": "adaptive"}, + } + assert top_level == {} + + def test_m3_openai_route_does_not_send_reasoning_effort(self): + import model_tools # noqa: F401 + import providers + + profile = providers.get_provider_profile("minimax") + assert profile is not None + extra_body, _top_level = profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "xhigh"}, + model="MiniMax-M3", + base_url="https://api.minimax.io/v1/", + ) + assert extra_body == { + "reasoning_split": True, + "thinking": {"type": "adaptive"}, + } + + def test_m3_openai_route_can_disable_thinking(self): + import model_tools # noqa: F401 + import providers + + profile = providers.get_provider_profile("minimax") + assert profile is not None + extra_body, top_level = profile.build_api_kwargs_extras( + reasoning_config={"enabled": False, "effort": "high"}, + model="MiniMax-M3", + base_url="https://api.minimax.io/v1", + ) + assert extra_body == { + "reasoning_split": True, + "thinking": {"type": "disabled"}, + } + assert top_level == {} + + @pytest.mark.parametrize( + "model,base_url", + [ + ("MiniMax-M2.7", "https://api.minimax.io/v1"), + ("MiniMax-M3", "https://api.minimax.io/anthropic"), + ("MiniMax-M3", "https://api.minimaxi.com/v1"), + ], + ) + def test_non_m3_or_non_global_openai_routes_emit_no_openai_reasoning_knobs( + self, model, base_url + ): + import model_tools # noqa: F401 + import providers + + profile = providers.get_provider_profile("minimax") + assert profile is not None + extra_body, top_level = profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "high"}, + model=model, + base_url=base_url, + ) + assert extra_body == {} + assert top_level == {} + + def test_transport_threads_base_url_to_profile(self): + import model_tools # noqa: F401 + import providers + from agent.transports.chat_completions import ChatCompletionsTransport + + profile = providers.get_provider_profile("minimax") + assert profile is not None + kwargs = ChatCompletionsTransport().build_kwargs( + model="MiniMax-M3", + messages=[{"role": "user", "content": "ping"}], + tools=None, + provider_profile=profile, + reasoning_config={"enabled": True, "effort": "medium"}, + base_url="https://api.minimax.io/v1", + ) + assert kwargs["extra_body"] == { + "reasoning_split": True, + "thinking": {"type": "adaptive"}, + }