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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user