diff --git a/.env.example b/.env.example index 2ebf6e4..6cf33b5 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,8 @@ GOOGLE_API_KEY= # aistudio.google.com/api-keys NVIDIA_API_KEY= # build.nvidia.com # 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 (智谱) VOLCENGINE_API_KEY= # volcengine.com (火山引擎) DASHSCOPE_API_KEY= # dashscope.aliyuncs.com (阿里云) diff --git a/EvoScientist/config/onboard.py b/EvoScientist/config/onboard.py index 49a15dc..d2ae56c 100644 --- a/EvoScientist/config/onboard.py +++ b/EvoScientist/config/onboard.py @@ -278,10 +278,19 @@ 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. +def validate_minimax_key( + 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: Tuple of (is_valid, message). @@ -294,16 +303,22 @@ def validate_minimax_key(api_key: str) -> tuple[bool, str]: client = anthropic.Anthropic( 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" 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}" @@ -758,6 +773,51 @@ def _step_provider(config: EvoScientistConfig) -> str: 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): """Return (display_name, current_value, validate_fn) for a provider.""" mapping = { @@ -769,7 +829,13 @@ def _provider_key_info(config: EvoScientistConfig, provider: str): "minimax": ( "MiniMax", 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", @@ -2819,7 +2885,7 @@ def run_onboard(skip_validation: bool = False) -> bool: provider = _step_provider(config) 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] = [] if provider == "custom-openai": 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) config.custom_anthropic_base_url = base_url + elif provider == "minimax": + config.minimax_base_url = _step_minimax_region(config) elif provider == "ollama": ollama_url, ollama_detected_models = _step_ollama_base_url(config) config.ollama_base_url = ollama_url diff --git a/EvoScientist/config/settings.py b/EvoScientist/config/settings.py index 12653e7..46402e3 100644 --- a/EvoScientist/config/settings.py +++ b/EvoScientist/config/settings.py @@ -67,6 +67,7 @@ class EvoScientistConfig: nvidia_api_key: str = "" google_api_key: str = "" minimax_api_key: str = "" + minimax_base_url: str = "" siliconflow_api_key: str = "" openrouter_api_key: str = "" deepseek_api_key: str = "" @@ -365,6 +366,7 @@ _ENV_MAPPINGS = { "nvidia_api_key": "NVIDIA_API_KEY", "google_api_key": "GOOGLE_API_KEY", "minimax_api_key": "MINIMAX_API_KEY", + "minimax_base_url": "MINIMAX_BASE_URL", "siliconflow_api_key": "SILICONFLOW_API_KEY", "openrouter_api_key": "OPENROUTER_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 if config.minimax_api_key and not os.environ.get("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"): os.environ["SILICONFLOW_API_KEY"] = config.siliconflow_api_key if config.openrouter_api_key and not os.environ.get("OPENROUTER_API_KEY"): diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index 0c68324..a7690f4 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -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-lite", "gemini-2.5-flash-lite", "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-highspeed", "MiniMax-M2.7-highspeed", "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)." ) base_url = base_url.rstrip("/") + elif provider == "minimax": + base_url = os.environ.get("MINIMAX_BASE_URL", base_url_default).rstrip("/") else: base_url = base_url_default if base_url: diff --git a/README.md b/README.md index 816195a..03392ac 100644 --- a/README.md +++ b/README.md @@ -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 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 (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 # Web search (optional) diff --git a/README.zh-CN.md b/README.zh-CN.md index e60583a..966706f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -273,7 +273,8 @@ EvoSci onboard 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 (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 # 网络搜索(可选) diff --git a/tests/test_llm.py b/tests/test_llm.py index 94972c0..2d92d6a 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -527,6 +527,18 @@ class TestThirdPartyRouting: assert call_kwargs["base_url"] == "https://api.minimaxi.com/anthropic" 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") def test_minimax_gets_thinking(self, mock_init, monkeypatch): """MiniMax provider should get auto-thinking (thinking-capable via Anthropic).""" diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py index 41371e0..a41bfbd 100644 --- a/tests/test_minimax_integration.py +++ b/tests/test_minimax_integration.py @@ -1,9 +1,10 @@ """Integration tests for MiniMax direct provider. 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. +Optionally set MINIMAX_BASE_URL to https://api.minimax.io/anthropic for Global keys. """ import os