diff --git a/README.md b/README.md
index bd282e9..5b4f431 100644
--- a/README.md
+++ b/README.md
@@ -88,13 +88,14 @@ Reasoning scatter (tokens/cost toggle in the viewer) vs. green rate.
```bash
export OPENROUTER_API_KEY=your_key_here
export OPENAI_API_KEY=your_openai_key_here # required only for models routed to OpenAI
+export MINIMAX_API_KEY=your_minimax_key_here # required only for models routed to MiniMax
export OPENAI_PROJECT=proj_xxx # optional: force OpenAI requests to a specific project
export OPENAI_ORGANIZATION=org_xxx # optional: force organization context
```
Provider routing is configured per model via `collect.model_providers` and
`grade.model_providers` in config (default is OpenRouter), for example:
-`{"*":"openrouter","gpt-5.3":"openai"}`.
+`{"*":"openrouter","gpt-5.3":"openai","minimax/*":"minimax"}`.
2. Run collection + primary judge (Claude by default):
diff --git a/config.json b/config.json
index 78ec435..d129cf0 100644
--- a/config.json
+++ b/config.json
@@ -50,13 +50,15 @@
"google/gemma-3-27b-it",
"qwen/qwen3.5-397b-a17b",
"moonshotai/kimi-k2.5",
- "minimax/minimax-m2.5"
+ "minimax/minimax-m2.7",
+ "minimax/minimax-m2.7-highspeed"
],
"models_file": "",
"model_providers": {
"*": "openrouter",
"openai/gpt-5.4-mini": "openai",
- "openai/gpt-5.4-nano": "openai"
+ "openai/gpt-5.4-nano": "openai",
+ "minimax/*": "minimax"
},
"num_runs": 1,
"parallelism": 12,
@@ -93,7 +95,8 @@
"z-ai/glm-5": ["none", "high"],
"qwen/qwen3.5-397b-a17b": ["none", "high"],
"moonshotai/kimi-k2.5": ["none", "high"],
- "minimax/minimax-m2.5": ["low", "high"]
+ "minimax/minimax-m2.7": ["none", "high"],
+ "minimax/minimax-m2.7-highspeed": ["none", "high"]
},
"shuffle_tasks": true
},
diff --git a/config.v2.json b/config.v2.json
index c6ed8b7..ba176f9 100644
--- a/config.v2.json
+++ b/config.v2.json
@@ -50,13 +50,15 @@
"google/gemma-3-27b-it",
"qwen/qwen3.5-397b-a17b",
"moonshotai/kimi-k2.5",
- "minimax/minimax-m2.5"
+ "minimax/minimax-m2.7",
+ "minimax/minimax-m2.7-highspeed"
],
"models_file": "",
"model_providers": {
"*": "openrouter",
"openai/gpt-5.4-mini": "openai",
- "openai/gpt-5.4-nano": "openai"
+ "openai/gpt-5.4-nano": "openai",
+ "minimax/*": "minimax"
},
"num_runs": 1,
"parallelism": 64,
@@ -175,8 +177,12 @@
"none",
"high"
],
- "minimax/minimax-m2.5": [
- "low",
+ "minimax/minimax-m2.7": [
+ "none",
+ "high"
+ ],
+ "minimax/minimax-m2.7-highspeed": [
+ "none",
"high"
]
},
diff --git a/scripts/openrouter_benchmark.py b/scripts/openrouter_benchmark.py
index a2bdee9..e13c6c4 100644
--- a/scripts/openrouter_benchmark.py
+++ b/scripts/openrouter_benchmark.py
@@ -65,11 +65,13 @@
"openrouter": "openrouter",
"or": "openrouter",
"openai": "openai",
+ "minimax": "minimax",
}
MODEL_PROVIDER_VALUES: tuple[str, ...] = (
"openrouter",
"openai",
+ "minimax",
)
DEFAULT_MODEL_PROVIDER = "openrouter"
@@ -2195,6 +2197,10 @@ class OpenAIAPIError(ProviderAPIError):
"""Errors from OpenAI Responses API calls."""
+class MiniMaxAPIError(ProviderAPIError):
+ """Errors from MiniMax chat/completions calls."""
+
+
class OpenRouterClient:
def __init__(self, api_key: str, timeout_seconds: int) -> None:
if timeout_seconds < 1:
@@ -2443,6 +2449,134 @@ def chat(
raise last_error
+def _minimax_model_id(model: str) -> str:
+ """Strip the ``minimax/`` namespace prefix if present."""
+ cleaned = str(model).strip()
+ if cleaned.startswith("minimax/"):
+ _, remainder = cleaned.split("/", 1)
+ if remainder:
+ return remainder
+ return cleaned
+
+
+def _minimax_clamp_temperature(temperature: float | None) -> float | None:
+ """MiniMax requires temperature in the open interval (0.0, 1.0]."""
+ if temperature is None:
+ return None
+ return max(0.01, min(float(temperature), 1.0))
+
+
+def _strip_think_tags(text: str) -> str:
+ """Remove ``…`` blocks that MiniMax M2.5+ may emit."""
+ return re.sub(r"[\s\S]*?", "", text).strip()
+
+
+class MiniMaxClient:
+ """Client for the MiniMax OpenAI-compatible chat/completions API."""
+
+ def __init__(self, api_key: str, timeout_seconds: int) -> None:
+ if timeout_seconds < 1:
+ raise ValueError("timeout_seconds must be >= 1")
+ self.api_key = api_key
+ self.timeout_seconds = timeout_seconds
+ self.base_url = "https://api.minimax.io/v1/chat/completions"
+
+ def chat(
+ self,
+ *,
+ model: str,
+ messages: list[dict[str, str]],
+ temperature: float | None,
+ max_tokens: int,
+ retries: int,
+ extra_payload: dict[str, Any] | None = None,
+ ) -> dict[str, Any]:
+ payload: dict[str, Any] = {
+ "model": _minimax_model_id(model),
+ "messages": messages,
+ }
+ clamped_temp = _minimax_clamp_temperature(temperature)
+ if clamped_temp is not None:
+ payload["temperature"] = clamped_temp
+ if max_tokens > 0:
+ payload["max_tokens"] = max_tokens
+ if extra_payload:
+ # MiniMax supports reasoning via the ``reasoning`` key (same
+ # schema as OpenRouter). Provider-specific keys like
+ # ``provider`` are silently dropped.
+ for key, value in extra_payload.items():
+ if key in {"provider"}:
+ continue
+ payload[key] = value
+ encoded = json.dumps(payload).encode("utf-8")
+
+ headers = {
+ "Authorization": f"Bearer {self.api_key}",
+ "Content-Type": "application/json",
+ }
+
+ if retries < 1:
+ raise ValueError("retries must be >= 1")
+
+ last_error: Exception | None = None
+ for attempt in range(1, retries + 1):
+ retry_after_header: str | None = None
+ retry_after_seconds: float | None = None
+ request = urllib.request.Request(
+ self.base_url,
+ data=encoded,
+ headers=headers,
+ method="POST",
+ )
+ try:
+ with urllib.request.urlopen(request, timeout=self.timeout_seconds) as resp:
+ raw = resp.read().decode("utf-8")
+ parsed = json.loads(raw)
+ if not isinstance(parsed, dict):
+ raise RuntimeError("MiniMax returned non-object JSON.")
+ # Strip … blocks from the response text so
+ # that internal reasoning traces do not pollute benchmark
+ # answers or judge inputs.
+ choices = parsed.get("choices")
+ if isinstance(choices, list):
+ for choice in choices:
+ if not isinstance(choice, dict):
+ continue
+ msg = choice.get("message")
+ if isinstance(msg, dict) and isinstance(msg.get("content"), str):
+ msg["content"] = _strip_think_tags(msg["content"])
+ return parsed
+ except urllib.error.HTTPError as exc:
+ detail = exc.read().decode("utf-8", errors="ignore")
+ retry_after_header = exc.headers.get("Retry-After") if exc.headers else None
+ retry_after_seconds = parse_retry_after_seconds(retry_after_header)
+ retryable = is_retryable_http_status(exc.code)
+ last_error = MiniMaxAPIError(
+ f"HTTP {exc.code} from MiniMax (attempt {attempt}/{retries})"
+ f"{' [retryable]' if retryable else ' [non-retryable]'}: {detail}"
+ + (
+ f" (retry_after_seconds={retry_after_seconds})"
+ if retry_after_seconds is not None
+ else ""
+ ),
+ status_code=exc.code,
+ retryable=retryable,
+ retry_after_seconds=retry_after_seconds,
+ )
+ if not retryable:
+ raise last_error from exc
+ except Exception as exc: # pylint: disable=broad-except
+ last_error = RuntimeError(
+ f"MiniMax call failed (attempt {attempt}/{retries}): {exc}"
+ )
+
+ if attempt < retries:
+ time.sleep(compute_retry_delay_seconds(attempt, retry_after_header))
+
+ assert last_error is not None
+ raise last_error
+
+
def extract_model_text(api_response: dict[str, Any]) -> str:
if api_response.get("error"):
err = api_response.get("error")
@@ -3016,6 +3150,17 @@ def run_collect(args: argparse.Namespace) -> int:
project_id=openai_project_id,
organization_id=openai_organization_id,
)
+ if "minimax" in providers_in_use:
+ minimax_key = os.getenv("MINIMAX_API_KEY", "").strip()
+ if not minimax_key:
+ raise RuntimeError(
+ "MINIMAX_API_KEY is required for models routed to minimax "
+ "unless --dry-run is set."
+ )
+ clients["minimax"] = MiniMaxClient(
+ api_key=minimax_key,
+ timeout_seconds=args.timeout_seconds,
+ )
started = time.perf_counter()
records: list[dict[str, Any]] = list(checkpoint_records)
@@ -4115,6 +4260,17 @@ def run_grade(args: argparse.Namespace) -> int:
project_id=openai_project_id,
organization_id=openai_organization_id,
)
+ elif judge_provider == "minimax":
+ minimax_key = os.getenv("MINIMAX_API_KEY", "").strip()
+ if not minimax_key:
+ raise RuntimeError(
+ "MINIMAX_API_KEY is required for judge models routed to minimax "
+ "unless --dry-run is set."
+ )
+ clients["minimax"] = MiniMaxClient(
+ api_key=minimax_key,
+ timeout_seconds=args.timeout_seconds,
+ )
started = time.perf_counter()
grade_rows: list[dict[str, Any]] = list(checkpoint_rows)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py
new file mode 100644
index 0000000..9210be3
--- /dev/null
+++ b/tests/test_minimax_provider.py
@@ -0,0 +1,564 @@
+"""Unit and integration tests for the MiniMax direct provider.
+
+Tests cover:
+ - MiniMaxClient initialization and validation
+ - Model ID stripping (minimax/ prefix removal)
+ - Temperature clamping to (0.0, 1.0]
+ - Think-tag stripping from response text
+ - Provider alias and value registration
+ - Provider resolution with minimax/* wildcard
+ - MiniMaxAPIError classification
+ - Config loading with minimax provider routing
+ - Client wiring in collect/grade flows (dry-run)
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+import unittest
+from unittest.mock import MagicMock, patch
+from typing import Any
+from http.server import HTTPServer, BaseHTTPRequestHandler
+import threading
+
+# Ensure the scripts directory is importable
+sys.path.insert(
+ 0,
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts"),
+)
+
+import openrouter_benchmark as bench
+
+
+# ---------------------------------------------------------------------------
+# Unit tests
+# ---------------------------------------------------------------------------
+
+
+class TestMiniMaxModelId(unittest.TestCase):
+ """Test _minimax_model_id prefix stripping."""
+
+ def test_strips_minimax_prefix(self):
+ self.assertEqual(bench._minimax_model_id("minimax/minimax-m2.7"), "minimax-m2.7")
+
+ def test_strips_minimax_prefix_highspeed(self):
+ self.assertEqual(
+ bench._minimax_model_id("minimax/minimax-m2.7-highspeed"),
+ "minimax-m2.7-highspeed",
+ )
+
+ def test_no_prefix_passthrough(self):
+ self.assertEqual(bench._minimax_model_id("minimax-m2.7"), "minimax-m2.7")
+
+ def test_other_prefix_passthrough(self):
+ self.assertEqual(bench._minimax_model_id("openai/gpt-4"), "openai/gpt-4")
+
+ def test_empty_after_prefix(self):
+ self.assertEqual(bench._minimax_model_id("minimax/"), "minimax/")
+
+ def test_whitespace_stripped(self):
+ self.assertEqual(bench._minimax_model_id(" minimax/minimax-m2.7 "), "minimax-m2.7")
+
+
+class TestMiniMaxClampTemperature(unittest.TestCase):
+ """Test _minimax_clamp_temperature enforces (0.0, 1.0]."""
+
+ def test_none_passthrough(self):
+ self.assertIsNone(bench._minimax_clamp_temperature(None))
+
+ def test_valid_temperature(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(0.5), 0.5)
+
+ def test_clamp_zero_to_min(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(0.0), 0.01)
+
+ def test_clamp_negative_to_min(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(-1.0), 0.01)
+
+ def test_clamp_above_one(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(2.0), 1.0)
+
+ def test_exactly_one(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(1.0), 1.0)
+
+ def test_small_positive(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(0.01), 0.01)
+
+ def test_very_small_clamped(self):
+ self.assertAlmostEqual(bench._minimax_clamp_temperature(0.001), 0.01)
+
+
+class TestStripThinkTags(unittest.TestCase):
+ """Test _strip_think_tags removes internal reasoning blocks."""
+
+ def test_no_think_tags(self):
+ self.assertEqual(bench._strip_think_tags("Hello world"), "Hello world")
+
+ def test_single_think_block(self):
+ text = "internal reasoningThe answer is 42."
+ self.assertEqual(bench._strip_think_tags(text), "The answer is 42.")
+
+ def test_multiline_think_block(self):
+ text = "\nStep 1: consider\nStep 2: decide\n\nFinal answer."
+ self.assertEqual(bench._strip_think_tags(text), "Final answer.")
+
+ def test_multiple_think_blocks(self):
+ text = "firstHello secondworld"
+ self.assertEqual(bench._strip_think_tags(text), "Hello world")
+
+ def test_empty_think_block(self):
+ text = "Result"
+ self.assertEqual(bench._strip_think_tags(text), "Result")
+
+ def test_only_think_block(self):
+ text = "all reasoning"
+ self.assertEqual(bench._strip_think_tags(text), "")
+
+
+class TestProviderRegistration(unittest.TestCase):
+ """Test that minimax is registered as a valid provider."""
+
+ def test_minimax_in_aliases(self):
+ self.assertIn("minimax", bench.MODEL_PROVIDER_ALIASES)
+ self.assertEqual(bench.MODEL_PROVIDER_ALIASES["minimax"], "minimax")
+
+ def test_minimax_in_values(self):
+ self.assertIn("minimax", bench.MODEL_PROVIDER_VALUES)
+
+ def test_normalize_minimax(self):
+ self.assertEqual(
+ bench.normalize_model_provider("minimax", field_name="test"),
+ "minimax",
+ )
+
+ def test_normalize_minimax_case_insensitive(self):
+ self.assertEqual(
+ bench.normalize_model_provider("MiniMax", field_name="test"),
+ "minimax",
+ )
+
+
+class TestProviderResolution(unittest.TestCase):
+ """Test resolve_model_provider with minimax/* wildcard routing."""
+
+ def test_wildcard_routing(self):
+ overrides = {"*": "openrouter", "minimax/*": "minimax"}
+ result = bench.resolve_model_provider("minimax/minimax-m2.7", overrides)
+ self.assertEqual(result, "minimax")
+
+ def test_wildcard_routing_highspeed(self):
+ overrides = {"*": "openrouter", "minimax/*": "minimax"}
+ result = bench.resolve_model_provider(
+ "minimax/minimax-m2.7-highspeed", overrides
+ )
+ self.assertEqual(result, "minimax")
+
+ def test_exact_match_overrides_wildcard(self):
+ overrides = {
+ "*": "openrouter",
+ "minimax/*": "minimax",
+ "minimax/minimax-m2.7": "openrouter",
+ }
+ result = bench.resolve_model_provider("minimax/minimax-m2.7", overrides)
+ self.assertEqual(result, "openrouter")
+
+ def test_non_minimax_model_uses_default(self):
+ overrides = {"*": "openrouter", "minimax/*": "minimax"}
+ result = bench.resolve_model_provider("anthropic/claude-sonnet-4.6", overrides)
+ self.assertEqual(result, "openrouter")
+
+
+class TestParseModelProviders(unittest.TestCase):
+ """Test parse_model_providers with minimax entries."""
+
+ def test_parse_minimax_provider(self):
+ raw = '{"*": "openrouter", "minimax/*": "minimax"}'
+ result = bench.parse_model_providers(raw, field_name="test")
+ self.assertEqual(result["minimax/*"], "minimax")
+
+ def test_parse_dict_input(self):
+ raw = {"*": "openrouter", "minimax/*": "minimax"}
+ result = bench.parse_model_providers(raw, field_name="test")
+ self.assertEqual(result["minimax/*"], "minimax")
+
+
+class TestMiniMaxAPIError(unittest.TestCase):
+ """Test MiniMaxAPIError inherits from ProviderAPIError."""
+
+ def test_is_provider_error(self):
+ err = bench.MiniMaxAPIError(
+ "test error", status_code=429, retryable=True
+ )
+ self.assertIsInstance(err, bench.ProviderAPIError)
+ self.assertEqual(err.status_code, 429)
+ self.assertTrue(err.retryable)
+
+ def test_non_retryable(self):
+ err = bench.MiniMaxAPIError(
+ "bad request", status_code=400, retryable=False
+ )
+ self.assertFalse(err.retryable)
+
+
+class TestMiniMaxClientInit(unittest.TestCase):
+ """Test MiniMaxClient initialization and validation."""
+
+ def test_valid_init(self):
+ client = bench.MiniMaxClient(api_key="test-key", timeout_seconds=30)
+ self.assertEqual(client.api_key, "test-key")
+ self.assertEqual(client.timeout_seconds, 30)
+ self.assertEqual(client.base_url, "https://api.minimax.io/v1/chat/completions")
+
+ def test_invalid_timeout(self):
+ with self.assertRaises(ValueError):
+ bench.MiniMaxClient(api_key="test-key", timeout_seconds=0)
+
+ def test_negative_timeout(self):
+ with self.assertRaises(ValueError):
+ bench.MiniMaxClient(api_key="test-key", timeout_seconds=-1)
+
+
+class TestMiniMaxClientChat(unittest.TestCase):
+ """Test MiniMaxClient.chat payload construction and response handling."""
+
+ def _make_client(self) -> bench.MiniMaxClient:
+ return bench.MiniMaxClient(api_key="test-key", timeout_seconds=30)
+
+ @patch("urllib.request.urlopen")
+ def test_basic_chat_payload(self, mock_urlopen):
+ """Verify the outgoing payload structure."""
+ response_body = json.dumps(
+ {
+ "id": "test-id",
+ "choices": [
+ {"message": {"content": "Test response"}, "finish_reason": "stop"}
+ ],
+ "usage": {"prompt_tokens": 10, "completion_tokens": 5},
+ }
+ ).encode("utf-8")
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = response_body
+ mock_resp.__enter__ = lambda s: s
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ client = self._make_client()
+ result = client.chat(
+ model="minimax/minimax-m2.7",
+ messages=[{"role": "user", "content": "Hello"}],
+ temperature=0.7,
+ max_tokens=100,
+ retries=1,
+ )
+
+ self.assertEqual(result["id"], "test-id")
+ # Verify the request was made with correct URL
+ call_args = mock_urlopen.call_args
+ request_obj = call_args[0][0]
+ self.assertEqual(request_obj.full_url, "https://api.minimax.io/v1/chat/completions")
+ sent_payload = json.loads(request_obj.data)
+ self.assertEqual(sent_payload["model"], "minimax-m2.7")
+ self.assertAlmostEqual(sent_payload["temperature"], 0.7)
+ self.assertEqual(sent_payload["max_tokens"], 100)
+
+ @patch("urllib.request.urlopen")
+ def test_temperature_clamping_in_payload(self, mock_urlopen):
+ """Verify temperature=0 is clamped to 0.01."""
+ response_body = json.dumps(
+ {
+ "id": "test-id",
+ "choices": [
+ {"message": {"content": "response"}, "finish_reason": "stop"}
+ ],
+ "usage": {},
+ }
+ ).encode("utf-8")
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = response_body
+ mock_resp.__enter__ = lambda s: s
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ client = self._make_client()
+ client.chat(
+ model="minimax-m2.7",
+ messages=[{"role": "user", "content": "Hi"}],
+ temperature=0.0,
+ max_tokens=0,
+ retries=1,
+ )
+
+ sent_payload = json.loads(mock_urlopen.call_args[0][0].data)
+ self.assertAlmostEqual(sent_payload["temperature"], 0.01)
+
+ @patch("urllib.request.urlopen")
+ def test_think_tag_stripping_in_response(self, mock_urlopen):
+ """Verify tags are stripped from response content."""
+ response_body = json.dumps(
+ {
+ "id": "test-id",
+ "choices": [
+ {
+ "message": {
+ "content": "reasoning hereThe actual answer."
+ },
+ "finish_reason": "stop",
+ }
+ ],
+ "usage": {},
+ }
+ ).encode("utf-8")
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = response_body
+ mock_resp.__enter__ = lambda s: s
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ client = self._make_client()
+ result = client.chat(
+ model="minimax-m2.7",
+ messages=[{"role": "user", "content": "Test"}],
+ temperature=None,
+ max_tokens=0,
+ retries=1,
+ )
+
+ self.assertEqual(result["choices"][0]["message"]["content"], "The actual answer.")
+
+ @patch("urllib.request.urlopen")
+ def test_provider_key_dropped(self, mock_urlopen):
+ """Verify the 'provider' key from extra_payload is dropped."""
+ response_body = json.dumps(
+ {
+ "id": "test-id",
+ "choices": [
+ {"message": {"content": "ok"}, "finish_reason": "stop"}
+ ],
+ "usage": {},
+ }
+ ).encode("utf-8")
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = response_body
+ mock_resp.__enter__ = lambda s: s
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ client = self._make_client()
+ client.chat(
+ model="minimax-m2.7",
+ messages=[{"role": "user", "content": "Hi"}],
+ temperature=0.5,
+ max_tokens=0,
+ retries=1,
+ extra_payload={"provider": {"require_parameters": True}, "reasoning": {"effort": "high"}},
+ )
+
+ sent_payload = json.loads(mock_urlopen.call_args[0][0].data)
+ self.assertNotIn("provider", sent_payload)
+ self.assertEqual(sent_payload["reasoning"], {"effort": "high"})
+
+ def test_invalid_retries(self):
+ client = self._make_client()
+ with self.assertRaises(ValueError):
+ client.chat(
+ model="minimax-m2.7",
+ messages=[],
+ temperature=None,
+ max_tokens=0,
+ retries=0,
+ )
+
+ @patch("urllib.request.urlopen")
+ def test_null_temperature_omitted(self, mock_urlopen):
+ """Verify temperature=None means no temperature key in payload."""
+ response_body = json.dumps(
+ {
+ "id": "test-id",
+ "choices": [
+ {"message": {"content": "ok"}, "finish_reason": "stop"}
+ ],
+ "usage": {},
+ }
+ ).encode("utf-8")
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = response_body
+ mock_resp.__enter__ = lambda s: s
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ client = self._make_client()
+ client.chat(
+ model="minimax-m2.7",
+ messages=[{"role": "user", "content": "Hi"}],
+ temperature=None,
+ max_tokens=0,
+ retries=1,
+ )
+
+ sent_payload = json.loads(mock_urlopen.call_args[0][0].data)
+ self.assertNotIn("temperature", sent_payload)
+
+
+class TestConfigMiniMaxRouting(unittest.TestCase):
+ """Test that config files route minimax/* models to minimax provider."""
+
+ def test_config_json(self):
+ import pathlib
+ config_path = (
+ pathlib.Path(__file__).resolve().parent.parent / "config.json"
+ )
+ with open(config_path, encoding="utf-8") as f:
+ config = json.load(f)
+ models = config["collect"]["models"]
+ providers = config["collect"]["model_providers"]
+ self.assertIn("minimax/minimax-m2.7", models)
+ self.assertIn("minimax/minimax-m2.7-highspeed", models)
+ self.assertEqual(providers.get("minimax/*"), "minimax")
+
+ def test_config_v2_json(self):
+ import pathlib
+ config_path = (
+ pathlib.Path(__file__).resolve().parent.parent / "config.v2.json"
+ )
+ with open(config_path, encoding="utf-8") as f:
+ config = json.load(f)
+ models = config["collect"]["models"]
+ providers = config["collect"]["model_providers"]
+ self.assertIn("minimax/minimax-m2.7", models)
+ self.assertIn("minimax/minimax-m2.7-highspeed", models)
+ self.assertEqual(providers.get("minimax/*"), "minimax")
+
+ def test_reasoning_efforts_defined(self):
+ import pathlib
+ config_path = (
+ pathlib.Path(__file__).resolve().parent.parent / "config.json"
+ )
+ with open(config_path, encoding="utf-8") as f:
+ config = json.load(f)
+ efforts = config["collect"]["model_reasoning_efforts"]
+ self.assertIn("minimax/minimax-m2.7", efforts)
+ self.assertIn("minimax/minimax-m2.7-highspeed", efforts)
+ self.assertEqual(efforts["minimax/minimax-m2.7"], ["none", "high"])
+
+
+# ---------------------------------------------------------------------------
+# Integration tests
+# ---------------------------------------------------------------------------
+
+
+class _FakeMiniMaxHandler(BaseHTTPRequestHandler):
+ """Minimal HTTP handler mimicking the MiniMax chat/completions API."""
+
+ def do_POST(self):
+ content_length = int(self.headers.get("Content-Length", 0))
+ body = self.rfile.read(content_length)
+ req = json.loads(body)
+ model = req.get("model", "unknown")
+ response = {
+ "id": "integration-test-id",
+ "object": "chat.completion",
+ "model": model,
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": (
+ "internal reasoning"
+ f"Integration test response from {model}"
+ ),
+ },
+ "finish_reason": "stop",
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 15,
+ "completion_tokens": 10,
+ "total_tokens": 25,
+ },
+ }
+ payload = json.dumps(response).encode("utf-8")
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+
+ def log_message(self, format, *args):
+ pass # suppress log output during tests
+
+
+class TestMiniMaxClientIntegration(unittest.TestCase):
+ """Integration tests using a local HTTP server to simulate MiniMax API."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.server = HTTPServer(("127.0.0.1", 0), _FakeMiniMaxHandler)
+ cls.port = cls.server.server_address[1]
+ cls.server_thread = threading.Thread(target=cls.server.serve_forever)
+ cls.server_thread.daemon = True
+ cls.server_thread.start()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.server.shutdown()
+ cls.server_thread.join(timeout=5)
+
+ def _make_client(self) -> bench.MiniMaxClient:
+ client = bench.MiniMaxClient(api_key="test-key", timeout_seconds=10)
+ client.base_url = f"http://127.0.0.1:{self.port}/v1/chat/completions"
+ return client
+
+ def test_full_chat_roundtrip(self):
+ """End-to-end test: send chat request, get response, think tags stripped."""
+ client = self._make_client()
+ result = client.chat(
+ model="minimax/minimax-m2.7",
+ messages=[
+ {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "user", "content": "What is 2+2?"},
+ ],
+ temperature=0.7,
+ max_tokens=100,
+ retries=1,
+ )
+
+ self.assertEqual(result["id"], "integration-test-id")
+ self.assertEqual(result["model"], "minimax-m2.7")
+ content = result["choices"][0]["message"]["content"]
+ # Think tags should be stripped
+ self.assertNotIn("", content)
+ self.assertIn("Integration test response from minimax-m2.7", content)
+ self.assertEqual(result["usage"]["total_tokens"], 25)
+
+ def test_temperature_zero_clamped(self):
+ """Verify temperature=0 is clamped in actual request."""
+ client = self._make_client()
+ result = client.chat(
+ model="minimax-m2.7-highspeed",
+ messages=[{"role": "user", "content": "Test"}],
+ temperature=0.0,
+ max_tokens=50,
+ retries=1,
+ )
+ # Should succeed — the clamped temperature (0.01) is valid
+ self.assertEqual(result["model"], "minimax-m2.7-highspeed")
+
+ def test_reasoning_effort_forwarded(self):
+ """Verify reasoning effort is forwarded via extra_payload."""
+ client = self._make_client()
+ result = client.chat(
+ model="minimax-m2.7",
+ messages=[{"role": "user", "content": "Think hard about this."}],
+ temperature=0.5,
+ max_tokens=200,
+ retries=1,
+ extra_payload={"reasoning": {"effort": "high"}},
+ )
+ self.assertEqual(result["id"], "integration-test-id")
+
+
+if __name__ == "__main__":
+ unittest.main()