feat: add MiniMax as direct LLM provider (#70)

Add MiniMax (api.minimax.io/v1) as a first-class third-party provider,
enabling direct API access without routing through NVIDIA/SiliconFlow/
OpenRouter intermediaries. Includes M2.5 and M2.5-highspeed models
with 204K context window.

Changes:
- Register "minimax" in _THIRD_PARTY_PROVIDERS with MINIMAX_API_KEY
- Add MiniMax-M2.5 and MiniMax-M2.5-highspeed model entries
- Add minimax_api_key to config, env mappings, and env export
- Add MiniMax to onboarding wizard with API key validation
- Update .env.example, README.md, README.zh-CN.md
- Add 9 unit tests and 3 integration tests (all passing)

Co-authored-by: PR Bot <pr-bot@minimaxi.com>
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
Octopus
2026-03-19 08:02:40 -05:00
committed by GitHub
parent ac5ea37d14
commit a011dca693
8 changed files with 197 additions and 13 deletions
+3
View File
@@ -6,6 +6,9 @@ OPENAI_API_KEY= # platform.openai.com
GOOGLE_API_KEY= # aistudio.google.com/api-keys
NVIDIA_API_KEY= # build.nvidia.com
# MiniMax (optional)
MINIMAX_API_KEY= # platform.minimaxi.com
# Third-party providers (optional)
SILICONFLOW_API_KEY= # siliconflow.cn
OPENROUTER_API_KEY= # openrouter.ai
+36
View File
@@ -276,6 +276,35 @@ def validate_google_key(api_key: str) -> tuple[bool, str]:
return False, f"Error: {e}"
def validate_minimax_key(api_key: str) -> tuple[bool, str]:
"""Validate a MiniMax API key by making a test request.
Returns:
Tuple of (is_valid, message).
"""
if not api_key:
return True, "Skipped (no key provided)"
try:
import openai
client = openai.OpenAI(
api_key=api_key, base_url="https://api.minimax.io/v1"
)
client.models.list()
return True, "Valid"
except Exception as e:
error_str = str(e).lower()
if (
"401" in error_str
or "unauthorized" in error_str
or "invalid" in error_str
or "authentication" in error_str
):
return False, "Invalid API key"
return False, f"Error: {e}"
def validate_siliconflow_key(api_key: str) -> tuple[bool, str]:
"""Validate a SiliconFlow API key by making a test request.
@@ -576,6 +605,7 @@ def _step_provider(config: EvoScientistConfig) -> str:
Choice(title="Anthropic (Claude models — API / OAuth)", value="anthropic"),
Choice(title="OpenAI (GPT models — API / OAuth)", value="openai"),
Choice(title="Google GenAI (Gemini models)", value="google-genai"),
Choice(title="MiniMax (M2.5 models — 204K context)", value="minimax"),
Choice(title="NVIDIA (third party — limited free requests)", value="nvidia"),
Choice(
title="SiliconFlow (third party — GLM, Kimi, MiniMax, etc.)",
@@ -636,6 +666,11 @@ def _provider_key_info(config: EvoScientistConfig, provider: str):
config.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", ""),
validate_anthropic_key,
),
"minimax": (
"MiniMax",
config.minimax_api_key or os.environ.get("MINIMAX_API_KEY", ""),
validate_minimax_key,
),
"nvidia": (
"NVIDIA",
config.nvidia_api_key or os.environ.get("NVIDIA_API_KEY", ""),
@@ -2356,6 +2391,7 @@ def run_onboard(skip_validation: bool = False) -> bool:
# and for Anthropic/OpenAI pure OAuth — key provided by ccproxy)
_PROVIDER_KEY_ATTR = {
"anthropic": "anthropic_api_key",
"minimax": "minimax_api_key",
"nvidia": "nvidia_api_key",
"google-genai": "google_api_key",
"siliconflow": "siliconflow_api_key",
+4
View File
@@ -67,6 +67,7 @@ class EvoScientistConfig:
openai_auth_mode: str = "api_key" # "api_key" | "oauth"
nvidia_api_key: str = ""
google_api_key: str = ""
minimax_api_key: str = ""
siliconflow_api_key: str = ""
openrouter_api_key: str = ""
zhipu_api_key: str = ""
@@ -346,6 +347,7 @@ _ENV_MAPPINGS = {
"openai_auth_mode": "EVOSCIENTIST_OPENAI_AUTH_MODE",
"nvidia_api_key": "NVIDIA_API_KEY",
"google_api_key": "GOOGLE_API_KEY",
"minimax_api_key": "MINIMAX_API_KEY",
"siliconflow_api_key": "SILICONFLOW_API_KEY",
"openrouter_api_key": "OPENROUTER_API_KEY",
"zhipu_api_key": "ZHIPU_API_KEY",
@@ -427,6 +429,8 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
os.environ["NVIDIA_API_KEY"] = config.nvidia_api_key
if config.google_api_key and not os.environ.get("GOOGLE_API_KEY"):
os.environ["GOOGLE_API_KEY"] = config.google_api_key
if config.minimax_api_key and not os.environ.get("MINIMAX_API_KEY"):
os.environ["MINIMAX_API_KEY"] = config.minimax_api_key
if config.siliconflow_api_key and not os.environ.get("SILICONFLOW_API_KEY"):
os.environ["SILICONFLOW_API_KEY"] = config.siliconflow_api_key
if config.openrouter_api_key and not os.environ.get("OPENROUTER_API_KEY"):
+8 -3
View File
@@ -1,9 +1,9 @@
"""LLM model configuration based on LangChain init_chat_model.
This module provides a unified interface for creating chat model instances
with support for multiple providers (Anthropic, OpenAI, Google GenAI, NVIDIA,
SiliconFlow, OpenRouter, ZhipuAI, Volcengine, DashScope, Ollama, and custom
OpenAI-compatible endpoints) and convenient short names for common models.
with support for multiple providers (Anthropic, OpenAI, Google GenAI, MiniMax,
NVIDIA, SiliconFlow, OpenRouter, ZhipuAI, Volcengine, DashScope, Ollama, and
custom OpenAI-compatible endpoints) and convenient short names for common models.
"""
from __future__ import annotations
@@ -63,6 +63,7 @@ def strip_thinking_tags(content: str) -> str:
return _THINKING_TAG_RE.sub("", content)
_MINIMAX_BASE_URL = "https://api.minimax.io/v1"
_SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"
_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
_ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"
@@ -73,6 +74,7 @@ _DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
# Third-party providers routed through the OpenAI provider with a custom base_url.
# Maps provider name → (base_url or None, env var for API key).
_THIRD_PARTY_PROVIDERS: dict[str, tuple[str | None, str]] = {
"minimax": (_MINIMAX_BASE_URL, "MINIMAX_API_KEY"),
"siliconflow": (_SILICONFLOW_BASE_URL, "SILICONFLOW_API_KEY"),
"openrouter": (_OPENROUTER_BASE_URL, "OPENROUTER_API_KEY"),
"zhipu": (_ZHIPU_BASE_URL, "ZHIPU_API_KEY"),
@@ -125,6 +127,9 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
("gemini-2.5-flash", "gemini-2.5-flash", "google-genai"),
("gemini-2.5-flash-lite", "gemini-2.5-flash-lite", "google-genai"),
("gemini-2.5-pro", "gemini-2.5-pro", "google-genai"),
# MiniMax (direct API — api.minimax.io)
("minimax-m2.5", "MiniMax-M2.5", "minimax"),
("minimax-m2.5-highspeed", "MiniMax-M2.5-highspeed", "minimax"),
# NVIDIA
("nemotron-super", "nvidia/nemotron-3-super-120b-a12b", "nvidia"),
("nemotron-nano", "nvidia/nemotron-3-nano-30b-a3b", "nvidia"),
+6 -5
View File
@@ -88,7 +88,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
## ✨ Features
- **🤖 Multi-Agent Team** — 6 sub-agents (plan, research, code, debug, analyze, write) working in concert.
- **🧠 Persistent Memory** — Context, preferences, and findings survive across sessions.
- **🌐 Multi-Provider** — Anthropic, OpenAI, Google, NVIDIA — one config to switch.
- **🌐 Multi-Provider** — Anthropic, OpenAI, Google, MiniMax, NVIDIA — one config to switch.
- **📱 Multi-Channel** — CLI as the hub; Telegram, Slack, Feishu, WeChat, and more — one agent session.
- **🔬 Scientific Workflow** — Intake → plan → execute → evaluate → write → verify.
- **🔌 MCP & Skills** — Plug in MCP servers or install skills from GitHub on the fly.
@@ -227,10 +227,11 @@ Set at least one LLM provider key and (optionally) a search key:
```bash
# Pick one LLM provider
export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com
export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com
export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys
export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com
export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com
export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com
export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys
export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com
export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com
# Web search (optional)
export TAVILY_API_KEY="tvly-..." # app.tavily.com
+6 -5
View File
@@ -96,7 +96,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
- **🤖 多智能体协作** — 6 个子智能体(规划、调研、编码、调试、分析、写作)协同工作。
- **🧠 持久化记忆** — 上下文、偏好和研究发现跨会话保持。
- **🌐 多模型供应商** — Anthropic、OpenAI、Google、NVIDIA——一处配置,随时切换。
- **🌐 多模型供应商** — Anthropic、OpenAI、Google、MiniMax、NVIDIA——一处配置,随时切换。
- **📱 多渠道接入** — CLI 为中心;Telegram、Slack、飞书、微信等——共享同一智能体会话。
- **🔬 科学工作流** — 需求采集 → 规划 → 执行 → 评估 → 撰写 → 验证。
- **🔌 MCP 与 Skills** — 即插即用 MCP 服务器,或从 GitHub 一键安装技能包。
@@ -236,10 +236,11 @@ EvoSci onboard
```bash
# 选择一个 LLM 供应商
export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com
export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com
export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys
export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com
export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com
export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com
export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys
export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com
export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com
# 网络搜索(可选)
export TAVILY_API_KEY="tvly-..." # app.tavily.com
+90
View File
@@ -29,6 +29,7 @@ class TestModelsRegistry:
assert "anthropic" in providers
assert "openai" in providers
assert "google-genai" in providers
assert "minimax" in providers
assert "nvidia" in providers
assert "siliconflow" in providers
assert "openrouter" in providers
@@ -43,6 +44,7 @@ class TestModelsRegistry:
"anthropic",
"openai",
"google-genai",
"minimax",
"nvidia",
"siliconflow",
"openrouter",
@@ -478,6 +480,94 @@ class TestThirdPartyRouting:
)
assert call_kwargs["api_key"] == "ds-key-456"
@patch("EvoScientist.llm.models.init_chat_model")
def test_minimax_routes_through_openai(self, mock_init, monkeypatch):
"""MiniMax provider should route through OpenAI with correct base_url."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("MINIMAX_API_KEY", "mm-key-123")
get_chat_model("MiniMax-M2.5", provider="minimax")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "openai"
assert call_kwargs["base_url"] == "https://api.minimax.io/v1"
assert call_kwargs["api_key"] == "mm-key-123"
@patch("EvoScientist.llm.models.init_chat_model")
def test_minimax_no_reasoning(self, mock_init, monkeypatch):
"""MiniMax provider should NOT get auto-reasoning (routed via OpenAI)."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("MINIMAX_API_KEY", "mm-key")
get_chat_model("MiniMax-M2.5", provider="minimax")
call_kwargs = mock_init.call_args[1]
assert "reasoning" not in call_kwargs
@patch("EvoScientist.llm.models.init_chat_model")
def test_minimax_short_name_resolution(self, mock_init, monkeypatch):
"""MiniMax short names should resolve to correct model IDs."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("MINIMAX_API_KEY", "mm-key")
get_chat_model("minimax-m2.5", provider="minimax")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model"] == "MiniMax-M2.5"
assert call_kwargs["model_provider"] == "openai"
@patch("EvoScientist.llm.models.init_chat_model")
def test_minimax_highspeed_model(self, mock_init, monkeypatch):
"""MiniMax M2.5-highspeed model should resolve correctly."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("MINIMAX_API_KEY", "mm-key")
get_chat_model("minimax-m2.5-highspeed", provider="minimax")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model"] == "MiniMax-M2.5-highspeed"
assert call_kwargs["model_provider"] == "openai"
assert call_kwargs["base_url"] == "https://api.minimax.io/v1"
# =============================================================================
# Test MiniMax provider
# =============================================================================
class TestMiniMaxProvider:
def test_minimax_in_third_party_providers(self):
"""MiniMax should be registered in _THIRD_PARTY_PROVIDERS."""
from EvoScientist.llm.models import _THIRD_PARTY_PROVIDERS
assert "minimax" in _THIRD_PARTY_PROVIDERS
base_url, api_key_env = _THIRD_PARTY_PROVIDERS["minimax"]
assert base_url == "https://api.minimax.io/v1"
assert api_key_env == "MINIMAX_API_KEY"
def test_minimax_models_registered(self):
"""MiniMax should have direct model entries in _MODEL_ENTRIES."""
minimax_models = get_models_for_provider("minimax")
assert len(minimax_models) >= 2
model_names = {name for name, _ in minimax_models}
assert "minimax-m2.5" in model_names
assert "minimax-m2.5-highspeed" in model_names
def test_minimax_model_ids_correct(self):
"""MiniMax model IDs should match the official API model names."""
minimax_models = get_models_for_provider("minimax")
model_dict = {name: mid for name, mid in minimax_models}
assert model_dict["minimax-m2.5"] == "MiniMax-M2.5"
assert model_dict["minimax-m2.5-highspeed"] == "MiniMax-M2.5-highspeed"
def test_minimax_short_name_in_models_dict(self):
"""MiniMax short names should be accessible via the MODELS dict."""
# Note: MODELS dict uses last-entry-wins, so direct minimax entries
# may be overridden by nvidia/siliconflow/openrouter entries.
# Use get_models_for_provider() for provider-specific lookups.
minimax_models = get_models_for_provider("minimax")
assert len(minimax_models) > 0
# =============================================================================
# Test _apply_auto_config
+44
View File
@@ -0,0 +1,44 @@
"""Integration tests for MiniMax direct provider.
These tests validate that the MiniMax provider can connect to the real
MiniMax API (api.minimax.io/v1) and produce chat completions.
Requires MINIMAX_API_KEY environment variable to be set.
"""
import os
import pytest
from EvoScientist.llm import get_chat_model
pytestmark = pytest.mark.skipif(
not os.environ.get("MINIMAX_API_KEY"),
reason="MINIMAX_API_KEY not set",
)
class TestMiniMaxIntegration:
def test_minimax_m25_chat_completion(self):
"""Test that MiniMax M2.5 can produce a chat completion."""
model = get_chat_model("minimax-m2.5", provider="minimax", temperature=0)
response = model.invoke("Reply with exactly: hello")
assert response.content
assert len(response.content) > 0
def test_minimax_m25_highspeed_chat_completion(self):
"""Test that MiniMax M2.5-highspeed can produce a chat completion."""
model = get_chat_model(
"minimax-m2.5-highspeed", provider="minimax", temperature=0
)
response = model.invoke("Reply with exactly: world")
assert response.content
assert len(response.content) > 0
def test_minimax_with_full_model_id(self):
"""Test using the full model ID directly."""
model = get_chat_model("MiniMax-M2.5", provider="minimax", temperature=0)
response = model.invoke("What is 2+2? Answer with just the number.")
assert response.content
assert "4" in response.content