From 738794809702fd67a0a10ad810c1fd7bd3468978 Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 5 Jun 2026 15:16:46 +0800 Subject: [PATCH] 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 --- README.md | 4 +- openspace/.env.example | 2 +- openspace/config/README.md | 7 ++-- openspace/host_detection/nanobot.py | 2 +- tests/test_minimax_integration.py | 21 ++++++++-- tests/test_minimax_provider.py | 62 +++++++++++++++++++++-------- 6 files changed, 71 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b23c2dc..9192e79 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/openspace/.env.example b/openspace/.env.example index 4a81851..80bdb36 100644 --- a/openspace/.env.example +++ b/openspace/.env.example @@ -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) ────────────────────────────── diff --git a/openspace/config/README.md b/openspace/config/README.md index ca774d3..e801133 100644 --- a/openspace/config/README.md +++ b/openspace/config/README.md @@ -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) diff --git a/openspace/host_detection/nanobot.py b/openspace/host_detection/nanobot.py index 77e753f..25461f6 100644 --- a/openspace/host_detection/nanobot.py +++ b/openspace/host_detection/nanobot.py @@ -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",), ""), ] diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py index 264da41..85e48ce 100644 --- a/tests/test_minimax_integration.py +++ b/tests/test_minimax_integration.py @@ -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, diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 12cdc51..ba4cd9c 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -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")