Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand Down
1 change: 1 addition & 0 deletions intentkit/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions intentkit/models/llm.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions intentkit/models/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class LLMProvider(str, Enum):
ETERNAL = "eternal"
REIGENT = "reigent"
VENICE = "venice"
MINIMAX = "minimax"
OLLAMA = "ollama"
OPENAI_COMPATIBLE = "openai_compatible"

Expand All @@ -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
Expand All @@ -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,
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
28 changes: 18 additions & 10 deletions intentkit/models/llm_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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:
Expand All @@ -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),
]
Expand All @@ -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:
Expand Down
108 changes: 108 additions & 0 deletions tests/core/test_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,6 +34,7 @@ def test_llm_model_filtering():
LLMProvider.ETERNAL,
LLMProvider.REIGENT,
LLMProvider.VENICE,
LLMProvider.MINIMAX,
}

for model in models.values():
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Loading