From cf63c3fefa50157071ad8b2bca87509625100c7c Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:59:38 +0000 Subject: [PATCH] feat(minimax): add MiniMax-M3 default and model-aware thinking Expose MiniMax-M3 as the default minimax_chat deployment and keep MiniMax-M2.7 selectable. Resolve thinking per model: MiniMax-M2.7 requires always-on thinking, while MiniMax-M3 supports adaptive or disabled thinking controlled by the existing flag. Co-Authored-By: Claude Opus 4.8 (1M context) --- configs/_base_/default.yaml | 2 +- skillopt/model/common.py | 2 +- skillopt/model/minimax_backend.py | 26 +++++- tests/test_minimax_backend.py | 133 ++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 tests/test_minimax_backend.py diff --git a/configs/_base_/default.yaml b/configs/_base_/default.yaml index c8c068d1..dad8d924 100644 --- a/configs/_base_/default.yaml +++ b/configs/_base_/default.yaml @@ -49,7 +49,7 @@ model: # MiniMax backend settings (minimax_chat target) minimax_base_url: "" # https://api.minimax.io/v1 if blank minimax_api_key: "" - minimax_model: "MiniMax-M2.7" + minimax_model: "MiniMax-M3" minimax_temperature: "0.7" minimax_max_tokens: "8000" minimax_enable_thinking: "false" diff --git a/skillopt/model/common.py b/skillopt/model/common.py index 97e4be81..f4375e1b 100644 --- a/skillopt/model/common.py +++ b/skillopt/model/common.py @@ -26,7 +26,7 @@ "claude_code_exec": "claude-sonnet-4-6", "cursor_exec": "composer-2.5", "qwen_chat": "Qwen/Qwen3.5-4B", - "minimax_chat": "MiniMax-M2.7", + "minimax_chat": "MiniMax-M3", "openai_compatible": "gpt-4o-mini", } diff --git a/skillopt/model/minimax_backend.py b/skillopt/model/minimax_backend.py index 7d9a42cb..6c44ae15 100644 --- a/skillopt/model/minimax_backend.py +++ b/skillopt/model/minimax_backend.py @@ -37,6 +37,28 @@ default_model_for_backend("minimax_chat"), ) +# Per-model thinking capability. MiniMax-M2.7 requires always-on thinking, so it +# must ignore the shared MINIMAX_ENABLE_THINKING flag; MiniMax-M3 supports +# adaptive/disabled thinking, which the flag controls (disabled by default). +_MODEL_THINKING_MODES: dict[str, tuple[str, ...]] = { + "MiniMax-M3": ("adaptive", "disabled"), + "MiniMax-M2.7": ("always_on",), +} + + +def _resolve_enable_thinking(deployment: str) -> bool: + """Return the effective ``enable_thinking`` flag for ``deployment``. + + Models that only support always-on thinking force the flag on regardless of + the configured default; models that allow disabled/adaptive thinking honor + the shared ``ENABLE_THINKING`` setting. + """ + modes = _MODEL_THINKING_MODES.get(str(deployment or "").strip()) + if modes and "disabled" not in modes and "always_on" in modes: + return True + return ENABLE_THINKING + + _config_lock = threading.Lock() tracker = TokenTracker() @@ -144,7 +166,9 @@ def _chat_messages_impl( "messages": _json_safe(messages), "max_tokens": min(max_completion_tokens, MAX_TOKENS), } - payload["chat_template_kwargs"] = {"enable_thinking": ENABLE_THINKING} + payload["chat_template_kwargs"] = { + "enable_thinking": _resolve_enable_thinking(deployment or TARGET_DEPLOYMENT) + } if TEMPERATURE is not None: payload["temperature"] = TEMPERATURE if tools: diff --git a/tests/test_minimax_backend.py b/tests/test_minimax_backend.py new file mode 100644 index 00000000..f54e74f8 --- /dev/null +++ b/tests/test_minimax_backend.py @@ -0,0 +1,133 @@ +"""Tests for the OpenAI-compatible MiniMax chat backend.""" + +from __future__ import annotations + +import importlib.util +import json +import sys +import types +from collections.abc import Iterator +from typing import Any + +import pytest + + +class _FakeResponse: + def __init__(self, payload: dict[str, Any]) -> None: + self._payload = payload + + def __enter__(self) -> "_FakeResponse": + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + return None + + def read(self) -> bytes: + return json.dumps(self._payload).encode("utf-8") + + +class _UrlopenRecorder: + def __init__(self, content: str = "answer") -> None: + self.content = content + self.calls: list[dict[str, Any]] = [] + + def __call__(self, request: Any, timeout: float | None = None) -> _FakeResponse: + self.calls.append( + { + "payload": json.loads(request.data.decode("utf-8")), + "timeout": timeout, + } + ) + return _FakeResponse( + { + "choices": [ + {"message": {"content": self.content}, "finish_reason": "stop"} + ], + "usage": {"prompt_tokens": 2, "completion_tokens": 1, "total_tokens": 3}, + } + ) + + +class _OpenAIClientStub: + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.args = args + self.kwargs = kwargs + + +def _install_openai_stub() -> None: + if "openai" in sys.modules or importlib.util.find_spec("openai") is not None: + return + openai_stub = types.ModuleType("openai") + openai_stub.AzureOpenAI = _OpenAIClientStub + openai_stub.OpenAI = _OpenAIClientStub + sys.modules["openai"] = openai_stub + + +@pytest.fixture() +def minimax_backend() -> Iterator[Any]: + _install_openai_stub() + from skillopt.model import minimax_backend as backend + + snapshot = { + "ENABLE_THINKING": backend.ENABLE_THINKING, + "TARGET_DEPLOYMENT": backend.TARGET_DEPLOYMENT, + "API_KEY": backend.API_KEY, + "BASE_URL": backend.BASE_URL, + } + backend.reset_token_tracker() + yield backend + backend.reset_token_tracker() + for key, value in snapshot.items(): + setattr(backend, key, value) + + +def _record_urlopen(monkeypatch: pytest.MonkeyPatch, backend: Any) -> _UrlopenRecorder: + recorder = _UrlopenRecorder() + monkeypatch.setattr(backend.urllib.request, "urlopen", recorder) + return recorder + + +def test_default_deployment_is_current_model(minimax_backend: Any) -> None: + from skillopt.model.common import default_model_for_backend + + assert default_model_for_backend("minimax_chat") == "MiniMax-M3" + + +def test_always_on_model_forces_thinking( + monkeypatch: pytest.MonkeyPatch, minimax_backend: Any +) -> None: + minimax_backend.ENABLE_THINKING = False + minimax_backend.TARGET_DEPLOYMENT = "MiniMax-M2.7" + recorder = _record_urlopen(monkeypatch, minimax_backend) + + minimax_backend.chat_target("system", "user", retries=1) + + payload = recorder.calls[0]["payload"] + assert payload["model"] == "MiniMax-M2.7" + assert payload["chat_template_kwargs"] == {"enable_thinking": True} + + +def test_adaptive_model_respects_disabled_flag( + monkeypatch: pytest.MonkeyPatch, minimax_backend: Any +) -> None: + minimax_backend.ENABLE_THINKING = False + minimax_backend.TARGET_DEPLOYMENT = "MiniMax-M3" + recorder = _record_urlopen(monkeypatch, minimax_backend) + + minimax_backend.chat_target("system", "user", retries=1) + + payload = recorder.calls[0]["payload"] + assert payload["model"] == "MiniMax-M3" + assert payload["chat_template_kwargs"] == {"enable_thinking": False} + + +def test_adaptive_model_respects_enabled_flag( + monkeypatch: pytest.MonkeyPatch, minimax_backend: Any +) -> None: + minimax_backend.ENABLE_THINKING = True + minimax_backend.TARGET_DEPLOYMENT = "MiniMax-M3" + recorder = _record_urlopen(monkeypatch, minimax_backend) + + minimax_backend.chat_target("system", "user", retries=1) + + assert recorder.calls[0]["payload"]["chat_template_kwargs"] == {"enable_thinking": True}