From 83e947d03e4cc1111948c5922897467d797d1899 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Mon, 23 Mar 2026 20:55:46 +0800 Subject: [PATCH] feat: add MiniMax as LLM provider with auto-detection Add MiniMax M2.7 and M2.7-highspeed models to the STRIX_MODEL_MAP for strix/ prefix shortcuts, auto-detect MINIMAX_API_KEY and set the MiniMax API base URL (https://api.minimax.io/v1) when a MiniMax model is selected. Includes provider documentation page, overview/README updates, 17 unit tests and 3 integration tests covering model mapping, config resolution, API key auto-detection, and end-to-end streaming completion. --- README.md | 3 +- docs/llm-providers/minimax.mdx | 43 +++++++ docs/llm-providers/overview.mdx | 4 + strix/config/config.py | 13 ++ strix/llm/utils.py | 2 + tests/llm/test_minimax.py | 167 ++++++++++++++++++++++++++ tests/llm/test_minimax_integration.py | 78 ++++++++++++ 7 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 docs/llm-providers/minimax.mdx create mode 100644 tests/llm/test_minimax.py create mode 100644 tests/llm/test_minimax_integration.py diff --git a/README.md b/README.md index 8f5997c6d..653e24641 100644 --- a/README.md +++ b/README.md @@ -230,8 +230,9 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high, - [OpenAI GPT-5.4](https://openai.com/api/) — `openai/gpt-5.4` - [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6` - [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview` +- [MiniMax-M2.7](https://platform.minimax.io) — `openai/MiniMax-M2.7` (set `LLM_API_BASE=https://api.minimax.io/v1`) -See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models. +See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, MiniMax, and local models. ## Enterprise diff --git a/docs/llm-providers/minimax.mdx b/docs/llm-providers/minimax.mdx new file mode 100644 index 000000000..80ee39bed --- /dev/null +++ b/docs/llm-providers/minimax.mdx @@ -0,0 +1,43 @@ +--- +title: "MiniMax" +description: "Configure Strix with MiniMax models" +--- + +[MiniMax](https://www.minimax.io) provides powerful large language models with up to 1M token context windows through an OpenAI-compatible API. + +## Setup + +```bash +export STRIX_LLM="openai/MiniMax-M2.7" +export LLM_API_KEY="your-minimax-api-key" +export LLM_API_BASE="https://api.minimax.io/v1" +``` + +Or use the shorthand with automatic base URL detection: + +```bash +export STRIX_LLM="openai/MiniMax-M2.7" +export MINIMAX_API_KEY="your-minimax-api-key" +``` + +## Available Models + +| Model | Configuration | Context Window | +|-------|---------------|----------------| +| MiniMax-M2.7 | `openai/MiniMax-M2.7` | 1M tokens | +| MiniMax-M2.7-highspeed | `openai/MiniMax-M2.7-highspeed` | 1M tokens | + +**MiniMax-M2.7** is the latest flagship model with strong reasoning and coding capabilities. +**MiniMax-M2.7-highspeed** offers faster inference with slightly reduced quality. + +## Get API Key + +1. Go to [platform.minimax.io](https://platform.minimax.io) +2. Sign up or sign in +3. Navigate to API Keys and create a new key + +## Notes + +- MiniMax API is fully OpenAI-compatible, so it works via the `openai/` LiteLLM prefix +- Temperature range: 0.0 to 1.0 (inclusive) +- When `MINIMAX_API_KEY` is set and the model name contains "minimax", the API key and base URL are auto-detected diff --git a/docs/llm-providers/overview.mdx b/docs/llm-providers/overview.mdx index 8c0d5002e..14314896e 100644 --- a/docs/llm-providers/overview.mdx +++ b/docs/llm-providers/overview.mdx @@ -14,6 +14,7 @@ Set your model and API key: | GPT-5.4 | OpenAI | `openai/gpt-5.4` | | Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` | | Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` | +| MiniMax-M2.7 | MiniMax | `openai/MiniMax-M2.7` | ```bash export STRIX_LLM="openai/gpt-5.4" @@ -52,6 +53,9 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re GPT-5.4 via Azure. + + MiniMax-M2.7 models with 1M context. + Llama 4, Mistral, and self-hosted models. diff --git a/strix/config/config.py b/strix/config/config.py index 782101ddb..953a761fe 100644 --- a/strix/config/config.py +++ b/strix/config/config.py @@ -212,4 +212,17 @@ def resolve_llm_config() -> tuple[str | None, str | None, str | None]: or Config.get("ollama_api_base") ) + # Auto-detect MiniMax provider: use MINIMAX_API_KEY and set base URL + if _is_minimax_model(model): + if not api_key: + api_key = os.getenv("MINIMAX_API_KEY") + if not api_base: + api_base = "https://api.minimax.io/v1" + return model, api_key, api_base + + +def _is_minimax_model(model: str) -> bool: + """Check if the model name refers to a MiniMax model.""" + lower = model.lower() + return "minimax" in lower diff --git a/strix/llm/utils.py b/strix/llm/utils.py index 9771854f7..547ee0ea1 100644 --- a/strix/llm/utils.py +++ b/strix/llm/utils.py @@ -41,6 +41,8 @@ def normalize_tool_format(content: str) -> str: "gemini-3-flash-preview": "gemini/gemini-3-flash-preview", "glm-5": "openrouter/z-ai/glm-5", "glm-4.7": "openrouter/z-ai/glm-4.7", + "minimax-m2.7": "openai/MiniMax-M2.7", + "minimax-m2.7-highspeed": "openai/MiniMax-M2.7-highspeed", } diff --git a/tests/llm/test_minimax.py b/tests/llm/test_minimax.py new file mode 100644 index 000000000..55bc9f876 --- /dev/null +++ b/tests/llm/test_minimax.py @@ -0,0 +1,167 @@ +"""Tests for MiniMax model integration.""" + +import os + +import pytest + +from strix.config.config import Config, _is_minimax_model, resolve_llm_config +from strix.llm.config import LLMConfig +from strix.llm.utils import STRIX_MODEL_MAP, resolve_strix_model + + +class TestMiniMaxModelMap: + """Tests for MiniMax entries in STRIX_MODEL_MAP.""" + + def test_minimax_m27_in_model_map(self): + assert "minimax-m2.7" in STRIX_MODEL_MAP + assert STRIX_MODEL_MAP["minimax-m2.7"] == "openai/MiniMax-M2.7" + + def test_minimax_m27_highspeed_in_model_map(self): + assert "minimax-m2.7-highspeed" in STRIX_MODEL_MAP + assert STRIX_MODEL_MAP["minimax-m2.7-highspeed"] == "openai/MiniMax-M2.7-highspeed" + + +class TestMiniMaxModelResolution: + """Tests for resolving strix/ MiniMax models.""" + + def test_resolve_strix_minimax_m27(self): + api_model, canonical = resolve_strix_model("strix/minimax-m2.7") + assert api_model == "openai/minimax-m2.7" + assert canonical == "openai/MiniMax-M2.7" + + def test_resolve_strix_minimax_m27_highspeed(self): + api_model, canonical = resolve_strix_model("strix/minimax-m2.7-highspeed") + assert api_model == "openai/minimax-m2.7-highspeed" + assert canonical == "openai/MiniMax-M2.7-highspeed" + + def test_resolve_direct_minimax_model_passthrough(self): + api_model, canonical = resolve_strix_model("openai/MiniMax-M2.7") + assert api_model == "openai/MiniMax-M2.7" + assert canonical == "openai/MiniMax-M2.7" + + +class TestIsMiniMaxModel: + """Tests for MiniMax model detection.""" + + def test_detects_minimax_openai_prefix(self): + assert _is_minimax_model("openai/MiniMax-M2.7") + + def test_detects_minimax_case_insensitive(self): + assert _is_minimax_model("openai/minimax-m2.7") + + def test_detects_minimax_strix_prefix(self): + assert _is_minimax_model("strix/minimax-m2.7") + + def test_non_minimax_model(self): + assert not _is_minimax_model("openai/gpt-5.4") + + def test_non_minimax_anthropic(self): + assert not _is_minimax_model("anthropic/claude-sonnet-4-6") + + +class TestMiniMaxConfigResolution: + """Tests for MiniMax auto-detection in resolve_llm_config.""" + + def test_auto_detect_minimax_api_key(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7") + monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key") + monkeypatch.delenv("LLM_API_KEY", raising=False) + monkeypatch.delenv("LLM_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + model, api_key, api_base = resolve_llm_config() + + assert model == "openai/MiniMax-M2.7" + assert api_key == "test-minimax-key" + assert api_base == "https://api.minimax.io/v1" + + def test_llm_api_key_takes_precedence_over_minimax_key(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7") + monkeypatch.setenv("LLM_API_KEY", "llm-key-takes-precedence") + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + monkeypatch.delenv("LLM_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + model, api_key, api_base = resolve_llm_config() + + assert api_key == "llm-key-takes-precedence" + assert api_base == "https://api.minimax.io/v1" + + def test_custom_api_base_takes_precedence(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7") + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setenv("LLM_API_BASE", "https://custom-proxy.com/v1") + monkeypatch.delenv("LLM_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + model, api_key, api_base = resolve_llm_config() + + assert api_base == "https://custom-proxy.com/v1" + + def test_no_minimax_key_no_auto_detect(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "openai/gpt-5.4") + monkeypatch.delenv("LLM_API_KEY", raising=False) + monkeypatch.delenv("MINIMAX_API_KEY", raising=False) + monkeypatch.delenv("LLM_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + model, api_key, api_base = resolve_llm_config() + + assert model == "openai/gpt-5.4" + assert api_key is None + assert api_base is None + + def test_minimax_auto_base_url_when_no_base_set(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7-highspeed") + monkeypatch.setenv("LLM_API_KEY", "some-key") + monkeypatch.delenv("LLM_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + model, api_key, api_base = resolve_llm_config() + + assert api_base == "https://api.minimax.io/v1" + + +class TestMiniMaxLLMConfig: + """Tests for LLMConfig with MiniMax models.""" + + def test_llm_config_minimax_direct(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7") + monkeypatch.setenv("LLM_API_KEY", "test-key") + monkeypatch.delenv("LLM_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + config = LLMConfig() + + assert config.model_name == "openai/MiniMax-M2.7" + assert config.litellm_model == "openai/MiniMax-M2.7" + assert config.api_key == "test-key" + assert config.api_base == "https://api.minimax.io/v1" + + def test_llm_config_minimax_strix_shortcut(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("STRIX_LLM", "strix/minimax-m2.7") + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + monkeypatch.delenv("LLM_API_KEY", raising=False) + monkeypatch.delenv("LLM_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_BASE", raising=False) + monkeypatch.delenv("LITELLM_BASE_URL", raising=False) + monkeypatch.delenv("OLLAMA_API_BASE", raising=False) + + config = LLMConfig() + + assert config.model_name == "strix/minimax-m2.7" + assert config.litellm_model == "openai/minimax-m2.7" + assert config.canonical_model == "openai/MiniMax-M2.7" + assert config.api_key == "minimax-key" diff --git a/tests/llm/test_minimax_integration.py b/tests/llm/test_minimax_integration.py new file mode 100644 index 000000000..eb9da2c09 --- /dev/null +++ b/tests/llm/test_minimax_integration.py @@ -0,0 +1,78 @@ +"""Integration tests for MiniMax provider. + +These tests verify end-to-end MiniMax integration by making real API calls. +They require MINIMAX_API_KEY to be set in the environment. +""" + +import os + +import pytest + +from strix.llm.config import LLMConfig +from strix.llm.llm import LLM + + +pytestmark = pytest.mark.skipif( + not os.environ.get("MINIMAX_API_KEY"), + reason="MINIMAX_API_KEY not set", +) + + +@pytest.fixture() +def minimax_llm(monkeypatch: pytest.MonkeyPatch) -> LLM: + """Create an LLM instance configured for MiniMax.""" + monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7") + monkeypatch.setenv("LLM_API_KEY", os.environ.get("MINIMAX_API_KEY", "")) + monkeypatch.setenv("LLM_API_BASE", "https://api.minimax.io/v1") + monkeypatch.setenv("STRIX_TELEMETRY", "0") + config = LLMConfig() + return LLM(config, agent_name=None) + + +@pytest.mark.asyncio() +async def test_minimax_basic_completion(minimax_llm: LLM): + """Test that MiniMax can complete a simple prompt.""" + messages = [{"role": "user", "content": "Reply with exactly: hello"}] + responses = [] + async for response in minimax_llm.generate(messages): + responses.append(response) + + assert len(responses) > 0 + final = responses[-1] + assert final.content + assert "hello" in final.content.lower() + + +@pytest.mark.asyncio() +async def test_minimax_streaming(minimax_llm: LLM): + """Test that MiniMax streaming produces incremental responses.""" + messages = [{"role": "user", "content": "Count from 1 to 3, one number per line."}] + responses = [] + async for response in minimax_llm.generate(messages): + responses.append(response) + + # Streaming should produce multiple intermediate responses + assert len(responses) >= 2 + final = responses[-1] + assert "1" in final.content + assert "2" in final.content + assert "3" in final.content + + +@pytest.mark.asyncio() +async def test_minimax_config_auto_detection(): + """Test that MINIMAX_API_KEY auto-detection works end-to-end.""" + api_key = os.environ.get("MINIMAX_API_KEY", "") + orig_llm_key = os.environ.pop("LLM_API_KEY", None) + orig_llm_base = os.environ.pop("LLM_API_BASE", None) + os.environ["STRIX_LLM"] = "openai/MiniMax-M2.7" + + try: + config = LLMConfig() + assert config.api_key == api_key + assert config.api_base == "https://api.minimax.io/v1" + finally: + if orig_llm_key is not None: + os.environ["LLM_API_KEY"] = orig_llm_key + if orig_llm_base is not None: + os.environ["LLM_API_BASE"] = orig_llm_base