fix(minimax): correct API endpoint and add region selection (#158)
This commit is contained in:
+2
-1
@@ -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 (阿里云)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
# 网络搜索(可选)
|
# 网络搜索(可选)
|
||||||
|
|||||||
@@ -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)."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user