diff --git a/.env.example b/.env.example index 3122bdc77..08aaae6ae 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ OPENAI_API_KEY= DEEPSEEK_API_KEY= XAI_API_KEY= GOOGLE_API_KEY= +MINIMAX_API_KEY= GOOGLE_GENAI_USE_VERTEXAI=false GOOGLE_CLOUD_PROJECT= diff --git a/intentkit/config/config.py b/intentkit/config/config.py index 1bf15ffdb..196d08ad8 100644 --- a/intentkit/config/config.py +++ b/intentkit/config/config.py @@ -151,6 +151,7 @@ def __init__(self) -> None: self.eternal_api_key: str | None = self.load("ETERNAL_API_KEY") self.reigent_api_key: str | None = self.load("REIGENT_API_KEY") self.venice_api_key: str | None = self.load("VENICE_API_KEY") + self.minimax_api_key: str | None = self.load("MINIMAX_API_KEY") self.openrouter_api_key: str | None = self.load("OPENROUTER_API_KEY") # OpenAI Compatible provider self.openai_compatible_api_key: str | None = self.load( diff --git a/intentkit/models/llm.csv b/intentkit/models/llm.csv index 76abf1f89..e8ba67886 100644 --- a/intentkit/models/llm.csv +++ b/intentkit/models/llm.csv @@ -2,6 +2,8 @@ id,name,provider,enabled,input_price,cached_input_price,output_price,price_level openrouter/free,OpenRouter Free,openrouter,TRUE,0,,0,1,200000,20000,2,2,FALSE,high,TRUE,TRUE,TRUE,300 openrouter/hunter-alpha,Hunter Alpha,openrouter,TRUE,0,,0,1,1048576,65536,4,2,FALSE,high,TRUE,TRUE,TRUE,300 openrouter/healer-alpha,Healer Alpha,openrouter,TRUE,0,,0,1,262144,65536,4,2,TRUE,high,TRUE,TRUE,TRUE,300 +MiniMax-M2.7,MiniMax M2.7,minimax,TRUE,0.3,0.03,1.2,2,204800,131072,5,3,FALSE,high,TRUE,TRUE,TRUE,300 +MiniMax-M2.7-highspeed,MiniMax M2.7 Highspeed,minimax,TRUE,0.07,,0.28,1,204800,131072,4,5,FALSE,none,TRUE,TRUE,TRUE,180 minimax/minimax-m2.7,MiniMax M2.7,openrouter,TRUE,0.3,0.03,1.2,2,204800,131072,5,3,FALSE,high,TRUE,TRUE,TRUE,300 minimax/minimax-m2-her,Minimax M2 Her,openrouter,TRUE,0.3,0.03,1.2,2,65536,2048,3,3,FALSE,none,TRUE,TRUE,TRUE,300 xiaomi/mimo-v2-pro,MiMo V2 Pro,openrouter,TRUE,1,,3,3,1048576,131072,5,2,FALSE,high,TRUE,TRUE,TRUE,300 diff --git a/intentkit/models/llm.py b/intentkit/models/llm.py index 6cc998136..a950402d3 100644 --- a/intentkit/models/llm.py +++ b/intentkit/models/llm.py @@ -190,6 +190,7 @@ class LLMProvider(str, Enum): ETERNAL = "eternal" REIGENT = "reigent" VENICE = "venice" + MINIMAX = "minimax" OLLAMA = "ollama" OPENAI_COMPATIBLE = "openai_compatible" @@ -205,6 +206,7 @@ def is_configured(self) -> bool: self.ETERNAL: bool(config.eternal_api_key), self.REIGENT: bool(config.reigent_api_key), self.VENICE: bool(config.venice_api_key), + self.MINIMAX: bool(config.minimax_api_key), self.OLLAMA: True, # Ollama usually doesn't need a key self.OPENAI_COMPATIBLE: bool( config.openai_compatible_api_key @@ -225,6 +227,7 @@ def display_name(self) -> str: self.ETERNAL: "Eternal", self.REIGENT: "Reigent", self.VENICE: "Venice", + self.MINIMAX: "MiniMax", self.OLLAMA: "Ollama", self.OPENAI_COMPATIBLE: config.openai_compatible_provider, } @@ -907,6 +910,38 @@ async def create_instance(self, params: dict[str, Any] = {}) -> BaseChatModel: return ChatOpenAI(**kwargs) +class MiniMaxLLM(LLMModel): + """MiniMax LLM configuration using OpenAI-compatible API.""" + + @override + async def create_instance(self, params: dict[str, Any] = {}) -> BaseChatModel: + """Create and return a ChatOpenAI instance configured for MiniMax.""" + from langchain_openai import ChatOpenAI + + info = await self.model_info() + + kwargs: dict[str, Any] = { + "model_name": info.id, + "openai_api_key": config.minimax_api_key, + "openai_api_base": "https://api.minimax.io/v1", + "timeout": info.timeout, + "max_retries": 3, + } + + if info.supports_temperature: + kwargs["temperature"] = self.temperature + + if info.supports_frequency_penalty: + kwargs["frequency_penalty"] = self.frequency_penalty + + if info.supports_presence_penalty: + kwargs["presence_penalty"] = self.presence_penalty + + kwargs.update(params) + + return ChatOpenAI(**kwargs) + + # Factory function to create the appropriate LLM model based on the model name async def create_llm_model( model_name: str, @@ -933,6 +968,7 @@ async def create_llm_model( LLMProvider.DEEPSEEK: DeepseekLLM, LLMProvider.XAI: XAILLM, LLMProvider.OPENROUTER: OpenRouterLLM, + LLMProvider.MINIMAX: MiniMaxLLM, LLMProvider.OLLAMA: OllamaLLM, LLMProvider.OPENAI: OpenAILLM, LLMProvider.OPENAI_COMPATIBLE: OpenAICompatibleLLM, diff --git a/intentkit/models/llm_picker.py b/intentkit/models/llm_picker.py index 9ae222d5b..a99117f96 100644 --- a/intentkit/models/llm_picker.py +++ b/intentkit/models/llm_picker.py @@ -12,13 +12,15 @@ def pick_summarize_model() -> str: """ # Priority order based on performance and cost effectiveness: # 1. Google Gemini 3 Flash: Best blend of speed and quality for summarization - # 2. GPT-5 Mini: High quality fallback - # 3. GLM 4.7 Flash: Fast and cheap alternative - # 4. Grok: Good performance if available - # 5. DeepSeek: Final fallback + # 2. MiniMax M2.7 Highspeed: Fast and cheap native API + # 3. GPT-5 Mini: High quality fallback + # 4. GLM 4.7 Flash: Fast and cheap alternative + # 5. Grok: Good performance if available + # 6. DeepSeek: Final fallback order: list[tuple[str, LLMProvider]] = [ ("z-ai/glm-4.7-flash", LLMProvider.OPENROUTER), ("gemini-3.1-flash-lite-preview", LLMProvider.GOOGLE), + ("MiniMax-M2.7-highspeed", LLMProvider.MINIMAX), ("gpt-5-mini", LLMProvider.OPENAI), ("grok-4-1-fast-non-reasoning", LLMProvider.XAI), ("deepseek-chat", LLMProvider.DEEPSEEK), @@ -29,6 +31,8 @@ def pick_summarize_model() -> str: return model_id if provider == LLMProvider.GOOGLE and config.google_api_key: return model_id + if provider == LLMProvider.MINIMAX and config.minimax_api_key: + return model_id if provider == LLMProvider.OPENROUTER and config.openrouter_api_key: return model_id if provider == LLMProvider.XAI and config.xai_api_key: @@ -45,15 +49,17 @@ def pick_default_model() -> str: Used as the default_factory for the agent model field. """ # Priority order based on general-purpose capability: - # 1. Google Gemini 3 Flash: Best blend of speed and quality - # 2. GPT-5 Mini: High quality fallback - # 3. MiniMax M2.5: Good general-purpose via OpenRouter - # 4. Grok: Good performance if available - # 5. DeepSeek: Final fallback + # 1. MiniMax M2.7: Top intelligence, native API preferred + # 2. Google Gemini 3 Flash: Best blend of speed and quality + # 3. GPT-5 Mini: High quality fallback + # 4. MiniMax M2.7 via OpenRouter: Good general-purpose fallback + # 5. Grok: Good performance if available + # 6. DeepSeek: Final fallback order: list[tuple[str, LLMProvider]] = [ - ("minimax/minimax-m2.5", LLMProvider.OPENROUTER), + ("MiniMax-M2.7", LLMProvider.MINIMAX), ("google/gemini-3-flash-preview", LLMProvider.GOOGLE), ("gpt-5-mini", LLMProvider.OPENAI), + ("minimax/minimax-m2.7", LLMProvider.OPENROUTER), ("grok-4-1-fast-non-reasoning", LLMProvider.XAI), ("deepseek-chat", LLMProvider.DEEPSEEK), ] @@ -63,6 +69,8 @@ def pick_default_model() -> str: return model_id if provider == LLMProvider.GOOGLE and config.google_api_key: return model_id + if provider == LLMProvider.MINIMAX and config.minimax_api_key: + return model_id if provider == LLMProvider.OPENROUTER and config.openrouter_api_key: return model_id if provider == LLMProvider.XAI and config.xai_api_key: diff --git a/tests/core/test_llm.py b/tests/core/test_llm.py index a3976140d..275c75def 100644 --- a/tests/core/test_llm.py +++ b/tests/core/test_llm.py @@ -17,6 +17,7 @@ def test_llm_model_filtering(): mock_config.eternal_api_key = None mock_config.reigent_api_key = None mock_config.venice_api_key = None + mock_config.minimax_api_key = None mock_config.openai_compatible_api_key = None mock_config.openai_compatible_base_url = None mock_config.openai_compatible_model = None @@ -33,6 +34,7 @@ def test_llm_model_filtering(): LLMProvider.ETERNAL, LLMProvider.REIGENT, LLMProvider.VENICE, + LLMProvider.MINIMAX, } for model in models.values(): @@ -51,6 +53,7 @@ def test_llm_model_filtering(): mock_config.eternal_api_key = None mock_config.reigent_api_key = None mock_config.venice_api_key = None + mock_config.minimax_api_key = None mock_config.openai_compatible_api_key = None mock_config.openai_compatible_base_url = None mock_config.openai_compatible_model = None @@ -76,6 +79,7 @@ def test_llm_model_filtering(): mock_config.eternal_api_key = None mock_config.reigent_api_key = None mock_config.venice_api_key = None + mock_config.minimax_api_key = None mock_config.openai_compatible_api_key = None mock_config.openai_compatible_base_url = None mock_config.openai_compatible_model = None @@ -98,6 +102,7 @@ def test_llm_model_filtering(): mock_config.eternal_api_key = None mock_config.reigent_api_key = None mock_config.venice_api_key = None + mock_config.minimax_api_key = None mock_config.openai_compatible_api_key = None mock_config.openai_compatible_base_url = None mock_config.openai_compatible_model = None @@ -124,6 +129,7 @@ def test_llm_model_filtering(): mock_config.eternal_api_key = None mock_config.reigent_api_key = None mock_config.venice_api_key = None + mock_config.minimax_api_key = None mock_config.openai_compatible_api_key = None mock_config.openai_compatible_base_url = None mock_config.openai_compatible_model = None @@ -153,6 +159,7 @@ def test_model_id_index_suffix_matching(): mock_config.eternal_api_key = None mock_config.reigent_api_key = None mock_config.venice_api_key = None + mock_config.minimax_api_key = None mock_config.openai_compatible_api_key = None mock_config.openai_compatible_base_url = None mock_config.openai_compatible_model = None @@ -171,3 +178,104 @@ def test_model_id_index_suffix_matching(): assert "gpt-5.4-mini" in index matching_keys = index["gpt-5.4-mini"] assert any("openrouter:" in k for k in matching_keys) + + +def test_minimax_models_loaded_with_key(): + """Test that native MiniMax models are loaded when MINIMAX_API_KEY is set.""" + with patch("intentkit.models.llm.config") as mock_config: + mock_config.openai_api_key = None + mock_config.google_api_key = None + mock_config.deepseek_api_key = None + mock_config.xai_api_key = None + mock_config.openrouter_api_key = None + mock_config.eternal_api_key = None + mock_config.reigent_api_key = None + mock_config.venice_api_key = None + mock_config.minimax_api_key = "mm-test-key" + mock_config.openai_compatible_api_key = None + mock_config.openai_compatible_base_url = None + mock_config.openai_compatible_model = None + + models = _load_default_llm_models() + + # Native MiniMax models should be present + minimax_models = [ + m for m in models.values() if m.provider == LLMProvider.MINIMAX + ] + assert len(minimax_models) >= 2, ( + "At least MiniMax-M2.7 and MiniMax-M2.7-highspeed should be loaded" + ) + + # Verify specific models exist + m27 = models.get("minimax:MiniMax-M2.7") + assert m27 is not None + assert m27.provider == LLMProvider.MINIMAX + assert m27.intelligence == 5 + + m27hs = models.get("minimax:MiniMax-M2.7-highspeed") + assert m27hs is not None + assert m27hs.provider == LLMProvider.MINIMAX + assert m27hs.speed == 5 + + # OpenRouter MiniMax models should NOT be present (no OpenRouter key) + or_minimax = [ + m + for m in models.values() + if m.provider == LLMProvider.OPENROUTER and "minimax" in m.id.lower() + ] + assert len(or_minimax) == 0 + + +def test_minimax_filtered_without_key(): + """Test that native MiniMax models are filtered when no key is set.""" + with patch("intentkit.models.llm.config") as mock_config: + mock_config.openai_api_key = None + mock_config.google_api_key = None + mock_config.deepseek_api_key = None + mock_config.xai_api_key = None + mock_config.openrouter_api_key = None + mock_config.eternal_api_key = None + mock_config.reigent_api_key = None + mock_config.venice_api_key = None + mock_config.minimax_api_key = None + mock_config.openai_compatible_api_key = None + mock_config.openai_compatible_base_url = None + mock_config.openai_compatible_model = None + + models = _load_default_llm_models() + + minimax_models = [ + m for m in models.values() if m.provider == LLMProvider.MINIMAX + ] + assert len(minimax_models) == 0, ( + "MiniMax models should be filtered out without API key" + ) + + +def test_minimax_and_openrouter_coexist(): + """Test that both native MiniMax and OpenRouter MiniMax models coexist.""" + with patch("intentkit.models.llm.config") as mock_config: + mock_config.openai_api_key = None + mock_config.google_api_key = None + mock_config.deepseek_api_key = None + mock_config.xai_api_key = None + mock_config.openrouter_api_key = "or-test-key" + mock_config.eternal_api_key = None + mock_config.reigent_api_key = None + mock_config.venice_api_key = None + mock_config.minimax_api_key = "mm-test-key" + mock_config.openai_compatible_api_key = None + mock_config.openai_compatible_base_url = None + mock_config.openai_compatible_model = None + + models = _load_default_llm_models() + + # Native MiniMax model + native = models.get("minimax:MiniMax-M2.7") + assert native is not None + assert native.provider == LLMProvider.MINIMAX + + # OpenRouter MiniMax model + openrouter = models.get("openrouter:minimax/minimax-m2.7") + assert openrouter is not None + assert openrouter.provider == LLMProvider.OPENROUTER diff --git a/tests/models/test_llm_picker.py b/tests/models/test_llm_picker.py index 508eabdad..8e8afb002 100644 --- a/tests/models/test_llm_picker.py +++ b/tests/models/test_llm_picker.py @@ -2,7 +2,10 @@ import pytest -from intentkit.models.llm_picker import pick_default_model, pick_summarize_model +from intentkit.models.llm_picker import ( + pick_default_model, + pick_summarize_model, +) # ── pick_summarize_model ───────────────────────────────────────────── @@ -14,10 +17,23 @@ def test_pick_summarize_model_prefers_google_then_openai(): mock_config.openrouter_api_key = None mock_config.xai_api_key = None mock_config.deepseek_api_key = None + mock_config.minimax_api_key = None assert pick_summarize_model() == "gpt-5-mini" +def test_pick_summarize_model_minimax_highspeed(): + with patch("intentkit.models.llm_picker.config") as mock_config: + mock_config.google_api_key = None + mock_config.openai_api_key = None + mock_config.openrouter_api_key = None + mock_config.xai_api_key = None + mock_config.deepseek_api_key = None + mock_config.minimax_api_key = "mm-key" + + assert pick_summarize_model() == "MiniMax-M2.7-highspeed" + + def test_pick_summarize_model_xai_when_available(): with patch("intentkit.models.llm_picker.config") as mock_config: mock_config.google_api_key = None @@ -25,6 +41,7 @@ def test_pick_summarize_model_xai_when_available(): mock_config.openrouter_api_key = None mock_config.xai_api_key = "xai-key" mock_config.deepseek_api_key = "ds-key" + mock_config.minimax_api_key = None assert pick_summarize_model() == "grok-4-1-fast-non-reasoning" @@ -36,6 +53,7 @@ def test_pick_summarize_model_raises_when_none(): mock_config.openrouter_api_key = None mock_config.xai_api_key = None mock_config.deepseek_api_key = None + mock_config.minimax_api_key = None with pytest.raises(RuntimeError): _ = pick_summarize_model() @@ -44,13 +62,26 @@ def test_pick_summarize_model_raises_when_none(): # ── pick_default_model ─────────────────────────────────────────────── -def test_pick_default_model_prefers_google(): +def test_pick_default_model_prefers_minimax_native(): + with patch("intentkit.models.llm_picker.config") as mock_config: + mock_config.google_api_key = "google-key" + mock_config.openai_api_key = "sk-test" + mock_config.openrouter_api_key = None + mock_config.xai_api_key = None + mock_config.deepseek_api_key = None + mock_config.minimax_api_key = "mm-key" + + assert pick_default_model() == "MiniMax-M2.7" + + +def test_pick_default_model_prefers_google_without_minimax(): with patch("intentkit.models.llm_picker.config") as mock_config: mock_config.google_api_key = "google-key" mock_config.openai_api_key = "sk-test" mock_config.openrouter_api_key = None mock_config.xai_api_key = None mock_config.deepseek_api_key = None + mock_config.minimax_api_key = None assert pick_default_model() == "google/gemini-3-flash-preview" @@ -62,8 +93,9 @@ def test_pick_default_model_openrouter_uses_minimax(): mock_config.openrouter_api_key = "or-key" mock_config.xai_api_key = None mock_config.deepseek_api_key = None + mock_config.minimax_api_key = None - assert pick_default_model() == "minimax/minimax-m2.5" + assert pick_default_model() == "minimax/minimax-m2.7" def test_pick_default_model_falls_to_deepseek(): @@ -73,6 +105,7 @@ def test_pick_default_model_falls_to_deepseek(): mock_config.openrouter_api_key = None mock_config.xai_api_key = None mock_config.deepseek_api_key = "ds-key" + mock_config.minimax_api_key = None assert pick_default_model() == "deepseek-chat" @@ -84,6 +117,7 @@ def test_pick_default_model_fallback_when_none(): mock_config.openrouter_api_key = None mock_config.xai_api_key = None mock_config.deepseek_api_key = None + mock_config.minimax_api_key = None result = pick_default_model() assert result == "gpt-5-mini" diff --git a/tests/models/test_minimax_llm.py b/tests/models/test_minimax_llm.py new file mode 100644 index 000000000..888b7529c --- /dev/null +++ b/tests/models/test_minimax_llm.py @@ -0,0 +1,277 @@ +"""Integration tests for MiniMax LLM provider.""" + +from decimal import Decimal +from unittest.mock import AsyncMock, patch + +import pytest + +from intentkit.models.llm import ( + LLMModel, + LLMModelInfo, + LLMProvider, + MiniMaxLLM, + create_llm_model, +) + + +def _make_minimax_model_info(**overrides): + """Create a MiniMax LLMModelInfo for testing.""" + from datetime import UTC, datetime + + defaults = { + "id": "MiniMax-M2.7", + "name": "MiniMax M2.7", + "provider": LLMProvider.MINIMAX, + "enabled": True, + "input_price": Decimal("0.3"), + "cached_input_price": Decimal("0.03"), + "output_price": Decimal("1.2"), + "context_length": 204800, + "output_length": 131072, + "intelligence": 5, + "speed": 3, + "supports_image_input": False, + "reasoning_effort": "high", + "supports_temperature": True, + "supports_frequency_penalty": True, + "supports_presence_penalty": True, + "timeout": 300, + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), + } + defaults.update(overrides) + return LLMModelInfo(**defaults) + + +class TestMiniMaxLLMProvider: + """Tests for the MiniMax LLM provider enum and configuration.""" + + def test_minimax_provider_exists(self): + assert LLMProvider.MINIMAX == "minimax" + + def test_minimax_display_name(self): + assert LLMProvider.MINIMAX.display_name() == "MiniMax" + + def test_minimax_configured_with_key(self): + with patch("intentkit.models.llm.config") as mock_config: + mock_config.minimax_api_key = "mm-test-key" + assert LLMProvider.MINIMAX.is_configured is True + + def test_minimax_not_configured_without_key(self): + with patch("intentkit.models.llm.config") as mock_config: + mock_config.minimax_api_key = None + assert LLMProvider.MINIMAX.is_configured is False + + def test_minimax_not_configured_with_empty_key(self): + with patch("intentkit.models.llm.config") as mock_config: + mock_config.minimax_api_key = "" + assert LLMProvider.MINIMAX.is_configured is False + + +class TestMiniMaxLLMClass: + """Tests for the MiniMaxLLM class.""" + + def test_minimax_llm_is_subclass_of_llm_model(self): + assert issubclass(MiniMaxLLM, LLMModel) + + @pytest.mark.asyncio + async def test_create_instance_uses_correct_base_url(self): + info = _make_minimax_model_info() + llm = MiniMaxLLM( + model_name="MiniMax-M2.7", + temperature=0.7, + frequency_penalty=0.0, + presence_penalty=0.0, + info=info, + ) + + with ( + patch("intentkit.models.llm.config") as mock_config, + patch.object( + LLMModelInfo, "get", new_callable=AsyncMock, return_value=info + ), + ): + mock_config.minimax_api_key = "mm-test-key" + instance = await llm.create_instance() + + assert instance.openai_api_base == "https://api.minimax.io/v1" + + @pytest.mark.asyncio + async def test_create_instance_uses_api_key(self): + info = _make_minimax_model_info() + llm = MiniMaxLLM( + model_name="MiniMax-M2.7", + temperature=0.7, + frequency_penalty=0.0, + presence_penalty=0.0, + info=info, + ) + + with ( + patch("intentkit.models.llm.config") as mock_config, + patch.object( + LLMModelInfo, "get", new_callable=AsyncMock, return_value=info + ), + ): + mock_config.minimax_api_key = "mm-test-key-123" + instance = await llm.create_instance() + + assert instance.openai_api_key.get_secret_value() == "mm-test-key-123" + + @pytest.mark.asyncio + async def test_create_instance_with_temperature(self): + info = _make_minimax_model_info(supports_temperature=True) + llm = MiniMaxLLM( + model_name="MiniMax-M2.7", + temperature=0.5, + frequency_penalty=0.0, + presence_penalty=0.0, + info=info, + ) + + with ( + patch("intentkit.models.llm.config") as mock_config, + patch.object( + LLMModelInfo, "get", new_callable=AsyncMock, return_value=info + ), + ): + mock_config.minimax_api_key = "mm-test-key" + instance = await llm.create_instance() + + assert instance.temperature == 0.5 + + @pytest.mark.asyncio + async def test_create_instance_without_temperature_support(self): + info = _make_minimax_model_info(supports_temperature=False) + llm = MiniMaxLLM( + model_name="MiniMax-M2.7", + temperature=0.9, + frequency_penalty=0.0, + presence_penalty=0.0, + info=info, + ) + + with ( + patch("intentkit.models.llm.config") as mock_config, + patch.object( + LLMModelInfo, "get", new_callable=AsyncMock, return_value=info + ), + ): + mock_config.minimax_api_key = "mm-test-key" + instance = await llm.create_instance() + + # Temperature should not be 0.9 since model doesn't support it + assert instance.temperature != 0.9 + + @pytest.mark.asyncio + async def test_create_instance_highspeed_model(self): + info = _make_minimax_model_info( + id="MiniMax-M2.7-highspeed", + name="MiniMax M2.7 Highspeed", + intelligence=4, + speed=5, + reasoning_effort=None, + ) + llm = MiniMaxLLM( + model_name="MiniMax-M2.7-highspeed", + temperature=0.7, + frequency_penalty=0.0, + presence_penalty=0.0, + info=info, + ) + + with ( + patch("intentkit.models.llm.config") as mock_config, + patch.object( + LLMModelInfo, "get", new_callable=AsyncMock, return_value=info + ), + ): + mock_config.minimax_api_key = "mm-test-key" + instance = await llm.create_instance() + + assert instance.model_name == "MiniMax-M2.7-highspeed" + + @pytest.mark.asyncio + async def test_create_instance_params_override(self): + info = _make_minimax_model_info() + llm = MiniMaxLLM( + model_name="MiniMax-M2.7", + temperature=0.7, + frequency_penalty=0.0, + presence_penalty=0.0, + info=info, + ) + + with ( + patch("intentkit.models.llm.config") as mock_config, + patch.object( + LLMModelInfo, "get", new_callable=AsyncMock, return_value=info + ), + ): + mock_config.minimax_api_key = "mm-test-key" + instance = await llm.create_instance({"max_retries": 5}) + + assert instance.max_retries == 5 + + +class TestMiniMaxFactory: + """Tests for MiniMax integration with the LLM factory.""" + + @pytest.mark.asyncio + async def test_create_llm_model_returns_minimax(self): + info = _make_minimax_model_info() + + with patch.object(LLMModelInfo, "get", new_callable=AsyncMock) as mock_get: + mock_get.return_value = info + model = await create_llm_model("MiniMax-M2.7") + + assert isinstance(model, MiniMaxLLM) + assert model.model_name == "MiniMax-M2.7" + + @pytest.mark.asyncio + async def test_create_llm_model_minimax_with_custom_params(self): + info = _make_minimax_model_info() + + with patch.object(LLMModelInfo, "get", new_callable=AsyncMock) as mock_get: + mock_get.return_value = info + model = await create_llm_model( + "MiniMax-M2.7", + temperature=0.3, + frequency_penalty=0.1, + presence_penalty=0.2, + ) + + assert isinstance(model, MiniMaxLLM) + assert model.temperature == 0.3 + assert model.frequency_penalty == 0.1 + assert model.presence_penalty == 0.2 + + +class TestMiniMaxModelInfo: + """Tests for MiniMax model information.""" + + def test_minimax_m27_model_info(self): + info = _make_minimax_model_info() + assert info.provider == LLMProvider.MINIMAX + assert info.intelligence == 5 + assert info.context_length == 204800 + assert info.output_length == 131072 + assert info.input_price == Decimal("0.3") + assert info.output_price == Decimal("1.2") + + def test_minimax_m27_highspeed_model_info(self): + info = _make_minimax_model_info( + id="MiniMax-M2.7-highspeed", + name="MiniMax M2.7 Highspeed", + intelligence=4, + speed=5, + input_price=Decimal("0.07"), + cached_input_price=None, + output_price=Decimal("0.28"), + reasoning_effort=None, + timeout=180, + ) + assert info.provider == LLMProvider.MINIMAX + assert info.speed == 5 + assert info.intelligence == 4 + assert info.input_price == Decimal("0.07")