Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion configs/_base_/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion skillopt/model/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand Down
26 changes: 25 additions & 1 deletion skillopt/model/minimax_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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:
Expand Down
133 changes: 133 additions & 0 deletions tests/test_minimax_backend.py
Original file line number Diff line number Diff line change
@@ -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}