fix(minimax): correct API endpoint and add region selection (#158)

This commit is contained in:
dinos
2026-04-15 14:30:26 +02:00
committed by GitHub
parent c4237fecb8
commit 2961e5ee88
8 changed files with 107 additions and 17 deletions
+2 -1
View File
@@ -7,7 +7,8 @@ GOOGLE_API_KEY= # aistudio.google.com/api-keys
NVIDIA_API_KEY= # build.nvidia.com NVIDIA_API_KEY= # build.nvidia.com
# Direct providers (optional) # Direct providers (optional)
MINIMAX_API_KEY= # platform.minimaxi.com (Anthropic-compatible) MINIMAX_API_KEY= # platform.minimaxi.com (China, default) or platform.minimax.io (Global)
MINIMAX_BASE_URL= # https://api.minimaxi.com/anthropic (default) or https://api.minimax.io/anthropic
ZHIPU_API_KEY= # open.bigmodel.cn (智谱) ZHIPU_API_KEY= # open.bigmodel.cn (智谱)
VOLCENGINE_API_KEY= # volcengine.com (火山引擎) VOLCENGINE_API_KEY= # volcengine.com (火山引擎)
DASHSCOPE_API_KEY= # dashscope.aliyuncs.com (阿里云) DASHSCOPE_API_KEY= # dashscope.aliyuncs.com (阿里云)
+80 -12
View File
@@ -278,10 +278,19 @@ def validate_google_key(api_key: str) -> tuple[bool, str]:
return False, f"Error: {e}" return False, f"Error: {e}"
def validate_minimax_key(api_key: str) -> tuple[bool, str]: def validate_minimax_key(
"""Validate a MiniMax API key by making a test request. api_key: str,
base_url: str = "https://api.minimaxi.com/anthropic",
) -> tuple[bool, str]:
"""Validate a MiniMax API key without consuming tokens.
Uses the Anthropic-compatible endpoint at api.minimaxi.com. Sends a messages.create() with an empty model string. MiniMax checks
auth *before* validating request params, so a valid key returns 400
(bad model) while an invalid key returns 401.
Args:
api_key: The MiniMax API key to validate.
base_url: Anthropic-compatible endpoint (global or mainland China).
Returns: Returns:
Tuple of (is_valid, message). Tuple of (is_valid, message).
@@ -294,16 +303,22 @@ def validate_minimax_key(api_key: str) -> tuple[bool, str]:
client = anthropic.Anthropic( client = anthropic.Anthropic(
api_key=api_key, api_key=api_key,
base_url="https://api.minimaxi.com/anthropic", base_url=base_url,
) )
client.models.list() client.messages.create(
model="",
max_tokens=1,
messages=[{"role": "user", "content": "hi"}],
)
# Unexpected success — treat as valid
return True, "Valid"
except anthropic.AuthenticationError:
return False, "Invalid API key"
except anthropic.APIStatusError:
# Any non-auth HTTP error (400 bad model, 500 insufficient balance,
# etc.) means the key itself was accepted → treat as valid.
return True, "Valid" return True, "Valid"
except Exception as e: except Exception as e:
error_str = str(e).lower()
if any(
k in error_str for k in ("401", "unauthorized", "invalid", "authentication")
):
return False, "Invalid API key"
return False, f"Error: {e}" return False, f"Error: {e}"
@@ -758,6 +773,51 @@ def _step_provider(config: EvoScientistConfig) -> str:
return provider return provider
_MINIMAX_REGIONS: dict[str, str] = {
"global": "https://api.minimax.io/anthropic",
"cn": "https://api.minimaxi.com/anthropic",
}
def _step_minimax_region(config: EvoScientistConfig) -> str:
"""Step 2a (MiniMax): Select API region.
MiniMax has two regional endpoints — Global (api.minimax.io) and
Mainland China (api.minimaxi.com). API keys are region-bound.
Returns:
The selected base URL.
"""
current = config.minimax_base_url or os.environ.get("MINIMAX_BASE_URL", "")
if current == _MINIMAX_REGIONS["global"]:
default = "global"
else:
default = "cn"
region = questionary.select(
"Select MiniMax API region (must match where your key was created):",
choices=[
Choice(
title="Global (api.minimax.io — platform.minimax.io keys)",
value="global",
),
Choice(
title="Mainland China (api.minimaxi.com — platform.minimaxi.com keys)",
value="cn",
),
],
default=default,
style=WIZARD_STYLE,
qmark=QMARK,
use_indicator=True,
).ask()
if region is None:
raise KeyboardInterrupt()
return _MINIMAX_REGIONS[region]
def _provider_key_info(config: EvoScientistConfig, provider: str): def _provider_key_info(config: EvoScientistConfig, provider: str):
"""Return (display_name, current_value, validate_fn) for a provider.""" """Return (display_name, current_value, validate_fn) for a provider."""
mapping = { mapping = {
@@ -769,7 +829,13 @@ def _provider_key_info(config: EvoScientistConfig, provider: str):
"minimax": ( "minimax": (
"MiniMax", "MiniMax",
config.minimax_api_key or os.environ.get("MINIMAX_API_KEY", ""), config.minimax_api_key or os.environ.get("MINIMAX_API_KEY", ""),
validate_minimax_key, lambda key: validate_minimax_key(
key,
base_url=config.minimax_base_url
or os.environ.get(
"MINIMAX_BASE_URL", "https://api.minimaxi.com/anthropic"
),
),
), ),
"nvidia": ( "nvidia": (
"NVIDIA", "NVIDIA",
@@ -2819,7 +2885,7 @@ def run_onboard(skip_validation: bool = False) -> bool:
provider = _step_provider(config) provider = _step_provider(config)
config.provider = provider config.provider = provider
# Step 2a: Base URL (custom-openai, custom-anthropic, or ollama provider) # Step 2a: Base URL (custom-openai, custom-anthropic, minimax, or ollama)
ollama_detected_models: list[str] = [] ollama_detected_models: list[str] = []
if provider == "custom-openai": if provider == "custom-openai":
current_base_url = config.custom_openai_base_url or os.environ.get( current_base_url = config.custom_openai_base_url or os.environ.get(
@@ -2833,6 +2899,8 @@ def run_onboard(skip_validation: bool = False) -> bool:
) )
base_url = _step_base_url(config, current_value=current_base_url) base_url = _step_base_url(config, current_value=current_base_url)
config.custom_anthropic_base_url = base_url config.custom_anthropic_base_url = base_url
elif provider == "minimax":
config.minimax_base_url = _step_minimax_region(config)
elif provider == "ollama": elif provider == "ollama":
ollama_url, ollama_detected_models = _step_ollama_base_url(config) ollama_url, ollama_detected_models = _step_ollama_base_url(config)
config.ollama_base_url = ollama_url config.ollama_base_url = ollama_url
+4
View File
@@ -67,6 +67,7 @@ class EvoScientistConfig:
nvidia_api_key: str = "" nvidia_api_key: str = ""
google_api_key: str = "" google_api_key: str = ""
minimax_api_key: str = "" minimax_api_key: str = ""
minimax_base_url: str = ""
siliconflow_api_key: str = "" siliconflow_api_key: str = ""
openrouter_api_key: str = "" openrouter_api_key: str = ""
deepseek_api_key: str = "" deepseek_api_key: str = ""
@@ -365,6 +366,7 @@ _ENV_MAPPINGS = {
"nvidia_api_key": "NVIDIA_API_KEY", "nvidia_api_key": "NVIDIA_API_KEY",
"google_api_key": "GOOGLE_API_KEY", "google_api_key": "GOOGLE_API_KEY",
"minimax_api_key": "MINIMAX_API_KEY", "minimax_api_key": "MINIMAX_API_KEY",
"minimax_base_url": "MINIMAX_BASE_URL",
"siliconflow_api_key": "SILICONFLOW_API_KEY", "siliconflow_api_key": "SILICONFLOW_API_KEY",
"openrouter_api_key": "OPENROUTER_API_KEY", "openrouter_api_key": "OPENROUTER_API_KEY",
"deepseek_api_key": "DEEPSEEK_API_KEY", "deepseek_api_key": "DEEPSEEK_API_KEY",
@@ -455,6 +457,8 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
os.environ["GOOGLE_API_KEY"] = config.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"): if config.minimax_api_key and not os.environ.get("MINIMAX_API_KEY"):
os.environ["MINIMAX_API_KEY"] = config.minimax_api_key os.environ["MINIMAX_API_KEY"] = config.minimax_api_key
if config.minimax_base_url and not os.environ.get("MINIMAX_BASE_URL"):
os.environ["MINIMAX_BASE_URL"] = config.minimax_base_url
if config.siliconflow_api_key and not os.environ.get("SILICONFLOW_API_KEY"): if config.siliconflow_api_key and not os.environ.get("SILICONFLOW_API_KEY"):
os.environ["SILICONFLOW_API_KEY"] = config.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"): if config.openrouter_api_key and not os.environ.get("OPENROUTER_API_KEY"):
+3 -1
View File
@@ -102,7 +102,7 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
("gemini-2.5-flash", "gemini-2.5-flash", "google-genai"), ("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-flash-lite", "gemini-2.5-flash-lite", "google-genai"),
("gemini-2.5-pro", "gemini-2.5-pro", "google-genai"), ("gemini-2.5-pro", "gemini-2.5-pro", "google-genai"),
# MiniMax (direct API — Anthropic-compatible at api.minimaxi.com) # MiniMax (direct API — Anthropic-compatible; default: api.minimaxi.com, global: api.minimax.io)
("minimax-m2.7", "MiniMax-M2.7", "minimax"), ("minimax-m2.7", "MiniMax-M2.7", "minimax"),
("minimax-m2.7-highspeed", "MiniMax-M2.7-highspeed", "minimax"), ("minimax-m2.7-highspeed", "MiniMax-M2.7-highspeed", "minimax"),
("minimax-m2.5", "MiniMax-M2.5", "minimax"), ("minimax-m2.5", "MiniMax-M2.5", "minimax"),
@@ -392,6 +392,8 @@ def get_chat_model(
"Anthropic-compatible API endpoint URL (e.g. https://api.anthropic.com)." "Anthropic-compatible API endpoint URL (e.g. https://api.anthropic.com)."
) )
base_url = base_url.rstrip("/") base_url = base_url.rstrip("/")
elif provider == "minimax":
base_url = os.environ.get("MINIMAX_BASE_URL", base_url_default).rstrip("/")
else: else:
base_url = base_url_default base_url = base_url_default
if base_url: if base_url:
+2 -1
View File
@@ -264,7 +264,8 @@ Set at least one LLM provider key and (optionally) a search key:
export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com
export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com
export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys
export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com (Anthropic-compatible) export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com (China, default) or platform.minimax.io (Global)
export MINIMAX_BASE_URL="https://api.minimax.io/anthropic" # only needed for Global keys (default: https://api.minimaxi.com/anthropic)
export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com
# Web search (optional) # Web search (optional)
+2 -1
View File
@@ -273,7 +273,8 @@ EvoSci onboard
export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com export ANTHROPIC_API_KEY="sk-..." # Claude — console.anthropic.com
export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com export OPENAI_API_KEY="sk-..." # GPT — platform.openai.com
export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys export GOOGLE_API_KEY="AI..." # Gemini — aistudio.google.com/api-keys
export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com (Anthropic-compatible) export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com(默认,中国大陆)或 platform.minimax.io(国际版)
export MINIMAX_BASE_URL="https://api.minimax.io/anthropic" # 仅国际版需要设置(默认: https://api.minimaxi.com/anthropic)
export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com
# 网络搜索(可选) # 网络搜索(可选)
+12
View File
@@ -527,6 +527,18 @@ class TestThirdPartyRouting:
assert call_kwargs["base_url"] == "https://api.minimaxi.com/anthropic" assert call_kwargs["base_url"] == "https://api.minimaxi.com/anthropic"
assert call_kwargs["api_key"] == "mm-key-123" assert call_kwargs["api_key"] == "mm-key-123"
@patch("EvoScientist.llm.models.init_chat_model")
def test_minimax_base_url_env_override(self, mock_init, monkeypatch):
"""MINIMAX_BASE_URL env var should override the default base URL."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("MINIMAX_API_KEY", "mm-key-123")
monkeypatch.setenv("MINIMAX_BASE_URL", "https://api.minimax.io/anthropic")
get_chat_model("MiniMax-M2.5", provider="minimax")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["base_url"] == "https://api.minimax.io/anthropic"
@patch("EvoScientist.llm.models.init_chat_model") @patch("EvoScientist.llm.models.init_chat_model")
def test_minimax_gets_thinking(self, mock_init, monkeypatch): def test_minimax_gets_thinking(self, mock_init, monkeypatch):
"""MiniMax provider should get auto-thinking (thinking-capable via Anthropic).""" """MiniMax provider should get auto-thinking (thinking-capable via Anthropic)."""
+2 -1
View File
@@ -1,9 +1,10 @@
"""Integration tests for MiniMax direct provider. """Integration tests for MiniMax direct provider.
These tests validate that the MiniMax provider can connect to the real These tests validate that the MiniMax provider can connect to the real
MiniMax API (api.minimaxi.com/anthropic) and produce chat completions. MiniMax API (api.minimaxi.com/anthropic by default) and produce chat completions.
Requires MINIMAX_API_KEY environment variable to be set. Requires MINIMAX_API_KEY environment variable to be set.
Optionally set MINIMAX_BASE_URL to https://api.minimax.io/anthropic for Global keys.
""" """
import os import os