feat: upgrade MiniMax default model to M3

- Add MiniMax-M3 to docs/examples and set as default model
- Keep MiniMax-M2.7 and MiniMax-M2.7-highspeed as alternatives
- Update provider registry comment to mention M3
- Expand unit/integration tests to cover M3 alongside legacy models

Co-Authored-By: Octopus <liyuan851277048@icloud.com>
This commit is contained in:
Octopus 2026-06-05 15:16:46 +08:00
parent 36005e3958
commit 7387948097
6 changed files with 71 additions and 27 deletions

View file

@ -210,8 +210,8 @@ openspace
# Execute task
openspace --model "anthropic/claude-sonnet-4-5" --query "Create a monitoring dashboard for my Docker containers"
# Use MiniMax (high-performance, 204K context)
openspace --model "minimax/MiniMax-M2.7" --query "Build a REST API with FastAPI"
# Use MiniMax (latest M3, 512K context with image input)
openspace --model "minimax/MiniMax-M3" --query "Build a REST API with FastAPI"
```
Add your own custom skills: [`openspace/skills/README.md`](openspace/skills/README.md).

View file

@ -17,7 +17,7 @@
# OpenRouter (for openrouter/* models, e.g. openrouter/anthropic/claude-sonnet-4.5)
OPENROUTER_API_KEY=
# MiniMax (for minimax/* models, e.g. minimax/MiniMax-M2.7)
# MiniMax (for minimax/* models, e.g. minimax/MiniMax-M3)
# MINIMAX_API_KEY=
# ── OpenSpace Cloud (optional) ──────────────────────────────

View file

@ -123,16 +123,17 @@ OpenSpace uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model routi
| Anthropic | `anthropic/claude-sonnet-4-5` | `ANTHROPIC_API_KEY` |
| OpenAI | `openai/gpt-4o` | `OPENAI_API_KEY` |
| DeepSeek | `deepseek/deepseek-chat` | `DEEPSEEK_API_KEY` |
| MiniMax | `minimax/MiniMax-M2.7` | `MINIMAX_API_KEY` |
| MiniMax | `minimax/MiniMax-M3` | `MINIMAX_API_KEY` |
### MiniMax
[MiniMax](https://platform.minimax.io) offers high-performance LLMs with 204K context at competitive pricing.
[MiniMax](https://platform.minimax.io) offers high-performance LLMs at competitive pricing.
**Available models:**
| Model | Context | Description |
|-------|---------|-------------|
| `MiniMax-M3` | 512K | Latest model, max output 128K, supports image input (default) |
| `MiniMax-M2.7` | 204K | Peak performance, ultimate value |
| `MiniMax-M2.7-highspeed` | 204K | Same performance, faster and more agile |
@ -143,7 +144,7 @@ OpenSpace uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model routi
export MINIMAX_API_KEY=your-key-here
# Run with MiniMax
openspace --model "minimax/MiniMax-M2.7" --query "your task"
openspace --model "minimax/MiniMax-M3" --query "your task"
```
**API docs:** [OpenAI-compatible API](https://platform.minimax.io/docs/api-reference/text-openai-api)

View file

@ -31,7 +31,7 @@ PROVIDER_REGISTRY: List[tuple] = [
("zhipu", ("zhipu", "glm", "zai"), ""),
("dashscope", ("qwen", "dashscope"), ""),
("moonshot", ("moonshot", "kimi"), "https://api.moonshot.ai/v1"),
("minimax", ("minimax",), "https://api.minimax.io/v1"), # MiniMax-M2.7, MiniMax-M2.7-highspeed
("minimax", ("minimax",), "https://api.minimax.io/v1"), # MiniMax-M3 (default), MiniMax-M2.7, MiniMax-M2.7-highspeed
("groq", ("groq",), ""),
]

View file

@ -16,8 +16,23 @@ SKIP_REASON = "MINIMAX_API_KEY not set"
class TestMiniMaxChatIntegration(unittest.TestCase):
"""Integration tests for MiniMax chat completions via litellm."""
def test_m3_chat_completion(self):
"""MiniMax M3 should return a valid chat completion."""
import litellm
response = litellm.completion(
model="minimax/MiniMax-M3",
messages=[{"role": "user", "content": "What is 2+2? Reply with just the number."}],
max_tokens=50,
temperature=0.5,
api_key=MINIMAX_API_KEY,
)
self.assertTrue(response.choices)
content = response.choices[0].message.content
self.assertIsNotNone(content)
def test_basic_chat_completion(self):
"""MiniMax M2.7 should return a valid chat completion."""
"""MiniMax M2.7 should still return a valid chat completion."""
import litellm
response = litellm.completion(
@ -46,11 +61,11 @@ class TestMiniMaxChatIntegration(unittest.TestCase):
self.assertTrue(response.choices[0].message.content)
def test_llm_client_with_minimax(self):
"""LLMClient should work with MiniMax models (temperature clamping applied)."""
"""LLMClient should work with MiniMax M3 (temperature clamping applied)."""
from openspace.llm.client import LLMClient
client = LLMClient(
model="minimax/MiniMax-M2.7-highspeed",
model="minimax/MiniMax-M3",
timeout=30.0,
max_retries=1,
api_key=MINIMAX_API_KEY,

View file

@ -15,6 +15,9 @@ from openspace.host_detection.resolver import build_llm_kwargs
class TestIsMiniMaxModel(unittest.TestCase):
"""Tests for MiniMax model detection."""
def test_minimax_m3_model(self):
self.assertTrue(_is_minimax_model("minimax/MiniMax-M3"))
def test_minimax_prefixed_model(self):
self.assertTrue(_is_minimax_model("minimax/MiniMax-M2.7"))
@ -22,10 +25,10 @@ class TestIsMiniMaxModel(unittest.TestCase):
self.assertTrue(_is_minimax_model("minimax/MiniMax-M2.7-highspeed"))
def test_minimax_case_insensitive(self):
self.assertTrue(_is_minimax_model("MiniMax/MiniMax-M2.7"))
self.assertTrue(_is_minimax_model("MiniMax/MiniMax-M3"))
def test_minimax_in_openai_compat(self):
self.assertTrue(_is_minimax_model("openai/MiniMax-M2.7"))
self.assertTrue(_is_minimax_model("openai/MiniMax-M3"))
def test_non_minimax_openai_model(self):
self.assertFalse(_is_minimax_model("openai/gpt-4o"))
@ -45,44 +48,44 @@ class TestApplyMiniMaxConstraints(unittest.TestCase):
def test_clamp_temperature_zero(self):
"""Temperature 0 should be clamped to 0.01."""
kwargs = {"model": "minimax/MiniMax-M2.7", "temperature": 0}
kwargs = {"model": "minimax/MiniMax-M3", "temperature": 0}
result = _apply_minimax_constraints(kwargs)
self.assertEqual(result["temperature"], 0.01)
def test_clamp_temperature_negative(self):
"""Negative temperature should be clamped to 0.01."""
kwargs = {"model": "minimax/MiniMax-M2.7", "temperature": -0.5}
kwargs = {"model": "minimax/MiniMax-M3", "temperature": -0.5}
result = _apply_minimax_constraints(kwargs)
self.assertEqual(result["temperature"], 0.01)
def test_clamp_temperature_above_one(self):
"""Temperature > 1.0 should be clamped to 1.0."""
kwargs = {"model": "minimax/MiniMax-M2.7", "temperature": 1.5}
kwargs = {"model": "minimax/MiniMax-M3", "temperature": 1.5}
result = _apply_minimax_constraints(kwargs)
self.assertEqual(result["temperature"], 1.0)
def test_valid_temperature_unchanged(self):
"""Valid temperature (0 < t <= 1.0) should remain unchanged."""
kwargs = {"model": "minimax/MiniMax-M2.7", "temperature": 0.7}
kwargs = {"model": "minimax/MiniMax-M3", "temperature": 0.7}
result = _apply_minimax_constraints(kwargs)
self.assertEqual(result["temperature"], 0.7)
def test_temperature_one_unchanged(self):
"""Temperature 1.0 is valid and should remain unchanged."""
kwargs = {"model": "minimax/MiniMax-M2.7", "temperature": 1.0}
kwargs = {"model": "minimax/MiniMax-M3", "temperature": 1.0}
result = _apply_minimax_constraints(kwargs)
self.assertEqual(result["temperature"], 1.0)
def test_no_temperature_no_change(self):
"""When temperature is not set, no clamping should occur."""
kwargs = {"model": "minimax/MiniMax-M2.7"}
kwargs = {"model": "minimax/MiniMax-M3"}
result = _apply_minimax_constraints(kwargs)
self.assertNotIn("temperature", result)
def test_remove_response_format(self):
"""response_format should be removed for MiniMax models."""
kwargs = {
"model": "minimax/MiniMax-M2.7",
"model": "minimax/MiniMax-M3",
"response_format": {"type": "json_object"},
}
result = _apply_minimax_constraints(kwargs)
@ -91,7 +94,7 @@ class TestApplyMiniMaxConstraints(unittest.TestCase):
def test_other_params_preserved(self):
"""Non-constrained parameters should be preserved."""
kwargs = {
"model": "minimax/MiniMax-M2.7",
"model": "minimax/MiniMax-M3",
"temperature": 0.5,
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}],
@ -101,6 +104,12 @@ class TestApplyMiniMaxConstraints(unittest.TestCase):
self.assertEqual(result["messages"], [{"role": "user", "content": "Hello"}])
self.assertEqual(result["temperature"], 0.5)
def test_constraints_apply_to_m27_legacy(self):
"""Constraints should also apply to legacy M2.7 models."""
kwargs = {"model": "minimax/MiniMax-M2.7", "temperature": 0}
result = _apply_minimax_constraints(kwargs)
self.assertEqual(result["temperature"], 0.01)
class TestProviderRegistry(unittest.TestCase):
"""Tests for MiniMax in the nanobot provider registry."""
@ -124,8 +133,18 @@ class TestProviderRegistry(unittest.TestCase):
self.assertIn("minimax", keywords)
break
def test_match_provider_minimax_m3(self):
"""match_provider should find minimax config for M3 models."""
providers = {
"minimax": {"apiKey": "test-key-m3"},
}
result = match_provider(providers, "minimax/MiniMax-M3")
self.assertIsNotNone(result)
self.assertEqual(result["api_key"], "test-key-m3")
self.assertEqual(result["api_base"], "https://api.minimax.io/v1")
def test_match_provider_minimax_model(self):
"""match_provider should find minimax config for minimax models."""
"""match_provider should find minimax config for legacy M2.7 models."""
providers = {
"minimax": {"apiKey": "test-key-123"},
}
@ -139,7 +158,7 @@ class TestProviderRegistry(unittest.TestCase):
providers = {
"minimax": {"apiKey": "test-key-456"},
}
result = match_provider(providers, "MiniMax-M2.7")
result = match_provider(providers, "MiniMax-M3")
self.assertIsNotNone(result)
self.assertEqual(result["api_key"], "test-key-456")
@ -159,8 +178,17 @@ class TestResolverMiniMaxDetection(unittest.TestCase):
@patch.dict(os.environ, {"MINIMAX_API_KEY": "minimax-test-key"}, clear=False)
@patch("openspace.host_detection.nanobot.try_read_nanobot_config", return_value=None)
def test_auto_detect_minimax_api_key(self, _mock_nanobot):
"""MINIMAX_API_KEY should be auto-detected for minimax models."""
def test_auto_detect_minimax_api_key_m3(self, _mock_nanobot):
"""MINIMAX_API_KEY should be auto-detected for minimax M3 models."""
model, kwargs = build_llm_kwargs("minimax/MiniMax-M3")
self.assertEqual(model, "minimax/MiniMax-M3")
self.assertEqual(kwargs.get("api_key"), "minimax-test-key")
self.assertEqual(kwargs.get("api_base"), "https://api.minimax.io/v1")
@patch.dict(os.environ, {"MINIMAX_API_KEY": "minimax-test-key"}, clear=False)
@patch("openspace.host_detection.nanobot.try_read_nanobot_config", return_value=None)
def test_auto_detect_minimax_api_key_m27(self, _mock_nanobot):
"""MINIMAX_API_KEY should still be auto-detected for legacy M2.7 models."""
model, kwargs = build_llm_kwargs("minimax/MiniMax-M2.7")
self.assertEqual(model, "minimax/MiniMax-M2.7")
self.assertEqual(kwargs.get("api_key"), "minimax-test-key")
@ -172,8 +200,8 @@ class TestResolverMiniMaxDetection(unittest.TestCase):
"""Without MINIMAX_API_KEY, no api_key should be set."""
# Remove MINIMAX_API_KEY if it exists
os.environ.pop("MINIMAX_API_KEY", None)
model, kwargs = build_llm_kwargs("minimax/MiniMax-M2.7")
self.assertEqual(model, "minimax/MiniMax-M2.7")
model, kwargs = build_llm_kwargs("minimax/MiniMax-M3")
self.assertEqual(model, "minimax/MiniMax-M3")
self.assertNotIn("api_key", kwargs)
@patch.dict(os.environ, {"MINIMAX_API_KEY": "minimax-key"}, clear=False)
@ -192,7 +220,7 @@ class TestResolverMiniMaxDetection(unittest.TestCase):
@patch("openspace.host_detection.nanobot.try_read_nanobot_config", return_value=None)
def test_explicit_key_overrides_minimax(self, _mock_nanobot):
"""OPENSPACE_LLM_API_KEY (Tier 1) should override MINIMAX_API_KEY (Tier 3)."""
model, kwargs = build_llm_kwargs("minimax/MiniMax-M2.7")
model, kwargs = build_llm_kwargs("minimax/MiniMax-M3")
self.assertEqual(kwargs["api_key"], "explicit-key")