diff --git a/.env.example b/.env.example index d3b13a3..249cac7 100644 --- a/.env.example +++ b/.env.example @@ -6,19 +6,21 @@ 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 -ZHIPU_API_KEY= # open.bigmodel.cn +# Direct providers (optional) +MINIMAX_API_KEY= # platform.minimaxi.com (Anthropic-compatible) +ZHIPU_API_KEY= # open.bigmodel.cn (智谱) VOLCENGINE_API_KEY= # volcengine.com (火山引擎) DASHSCOPE_API_KEY= # dashscope.aliyuncs.com (阿里云) -CUSTOM_OPENAI_API_KEY= # Third-party OpenAI-compatible endpoint -CUSTOM_OPENAI_BASE_URL= # OpenAI-compatible base URL (optional) -CUSTOM_ANTHROPIC_API_KEY= # Third-party Anthropic-compatible endpoint -CUSTOM_ANTHROPIC_BASE_URL= # Anthropic-compatible base URL + +# Aggregator platforms (optional) +SILICONFLOW_API_KEY= # siliconflow.cn +OPENROUTER_API_KEY= # openrouter.ai + +# Custom endpoints (optional) +CUSTOM_OPENAI_API_KEY= # OpenAI-compatible endpoint +CUSTOM_OPENAI_BASE_URL= +CUSTOM_ANTHROPIC_API_KEY= # Anthropic-compatible endpoint +CUSTOM_ANTHROPIC_BASE_URL= # Local models (optional) OLLAMA_BASE_URL= # http://localhost:11434 (default) diff --git a/EvoScientist/ccproxy_manager.py b/EvoScientist/ccproxy_manager.py index e6d3e9a..42cd37e 100644 --- a/EvoScientist/ccproxy_manager.py +++ b/EvoScientist/ccproxy_manager.py @@ -76,10 +76,7 @@ def _is_editable_install() -> bool: def _oauth_install_hint() -> str: """Return the appropriate install command depending on install method.""" if _is_editable_install(): - return ( - "uv sync --extra oauth " - "or pip install -e '.[oauth]'" - ) + return "uv sync --extra oauth or pip install -e '.[oauth]'" return "pip install 'evoscientist[oauth]'" @@ -189,7 +186,7 @@ def start_ccproxy(port: int) -> subprocess.Popen: The Popen handle for the ccproxy process. Raises: - RuntimeError: If ccproxy fails to become healthy within 10 seconds. + RuntimeError: If ccproxy fails to become healthy within 30 seconds. FileNotFoundError: If ccproxy binary is not found. """ exe = _ccproxy_exe() or "ccproxy" @@ -315,10 +312,13 @@ def _patch_ccproxy_oauth_header() -> None: # Ask that Python where ccproxy's adapter lives result = subprocess.run( [ - python_exe, "-c", + python_exe, + "-c", "import inspect, ccproxy.plugins.claude_api.adapter as m; print(inspect.getfile(m))", ], - capture_output=True, text=True, timeout=10, + capture_output=True, + text=True, + timeout=10, ) if result.returncode != 0: return @@ -345,10 +345,10 @@ def _patch_ccproxy_oauth_header() -> None: text, ) # Step 2: insert correct assignment after the cli_headers block - insert_after = 'filtered_headers[lk] = value\n' + insert_after = "filtered_headers[lk] = value\n" replacement = ( - 'filtered_headers[lk] = value\n\n' - ' # oauth-2025-04-20: required for OAuth Bearer token auth (Anthropic 2026-03)\n' + "filtered_headers[lk] = value\n\n" + " # oauth-2025-04-20: required for OAuth Bearer token auth (Anthropic 2026-03)\n" ' filtered_headers["anthropic-beta"] = "oauth-2025-04-20"\n' ) patched = patched.replace(insert_after, replacement, 1) diff --git a/EvoScientist/config/onboard.py b/EvoScientist/config/onboard.py index 4523bc1..3f0f018 100644 --- a/EvoScientist/config/onboard.py +++ b/EvoScientist/config/onboard.py @@ -279,6 +279,8 @@ def validate_google_key(api_key: str) -> tuple[bool, str]: def validate_minimax_key(api_key: str) -> tuple[bool, str]: """Validate a MiniMax API key by making a test request. + Uses the Anthropic-compatible endpoint at api.minimaxi.com. + Returns: Tuple of (is_valid, message). """ @@ -286,20 +288,18 @@ def validate_minimax_key(api_key: str) -> tuple[bool, str]: return True, "Skipped (no key provided)" try: - import openai + import anthropic - client = openai.OpenAI( - api_key=api_key, base_url="https://api.minimax.io/v1" + client = anthropic.Anthropic( + api_key=api_key, + base_url="https://api.minimaxi.com/anthropic", ) 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 + if any( + k in error_str for k in ("401", "unauthorized", "invalid", "authentication") ): return False, "Invalid API key" return False, f"Error: {e}" @@ -602,18 +602,12 @@ def _step_provider(config: EvoScientistConfig) -> str: Selected provider name. """ choices = [ + # Direct providers 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.)", - value="siliconflow", - ), - Choice( - title="OpenRouter (third party — Grok, Gemini, Qwen, etc.)", - value="openrouter", + title="MiniMax (M2 — M2.7 models, 204K context, thinking)", value="minimax" ), Choice(title="ZhipuAI (智谱 — GLM models)", value="zhipu"), Choice( @@ -628,7 +622,18 @@ def _step_provider(config: EvoScientistConfig) -> str: title="DashScope (阿里云 — Qwen models)", value="dashscope", ), + # Local Choice(title="Ollama (local models)", value="ollama"), + # Third-party / aggregator + Choice(title="NVIDIA (third party — limited free requests)", value="nvidia"), + Choice( + title="SiliconFlow (aggregator — GLM, Kimi, MiniMax, etc.)", + value="siliconflow", + ), + Choice( + title="OpenRouter (aggregator — Grok, Gemini, Qwen, etc.)", + value="openrouter", + ), Choice( title="OpenAI-compatible (third-party OpenAI endpoint)", value="custom-openai", @@ -1640,9 +1645,7 @@ def _step_mcp_servers() -> list[str]: return [] # Check if any selected servers require npx - needs_npx = any( - srv.command == "npx" for srv in servers if srv.name in selected - ) + needs_npx = any(srv.command == "npx" for srv in servers if srv.name in selected) if needs_npx: if not _ensure_npx("some MCP servers require Node.js"): npx_servers = { @@ -1666,7 +1669,9 @@ def _step_mcp_servers() -> list[str]: _print_step_result("MCP", f"{name}") installed.append(name) else: - _print_step_result("MCP", f"{name} — installation failed", success=False) + _print_step_result( + "MCP", f"{name} — installation failed", success=False + ) except Exception as e: _print_step_result("MCP", f"{name} — {e}", success=False) diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index a48b16e..ebd7be2 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -1,9 +1,10 @@ """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, MiniMax, -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 +(Anthropic-compatible), NVIDIA, SiliconFlow, OpenRouter, ZhipuAI, Volcengine, +DashScope, Ollama, and custom OpenAI/Anthropic-compatible endpoints) and +convenient short names for common models. """ from __future__ import annotations @@ -63,7 +64,7 @@ def strip_thinking_tags(content: str) -> str: return _THINKING_TAG_RE.sub("", content) -_MINIMAX_BASE_URL = "https://api.minimax.io/v1" +_MINIMAX_ANTHROPIC_BASE_URL = "https://api.minimaxi.com/anthropic" _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" @@ -71,10 +72,9 @@ _ZHIPU_CODE_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4" _VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3" _DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" -# Third-party providers routed through the OpenAI provider with a custom base_url. +# 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"), +_OPENAI_ROUTED_PROVIDERS: dict[str, tuple[str | None, str]] = { "siliconflow": (_SILICONFLOW_BASE_URL, "SILICONFLOW_API_KEY"), "openrouter": (_OPENROUTER_BASE_URL, "OPENROUTER_API_KEY"), "zhipu": (_ZHIPU_BASE_URL, "ZHIPU_API_KEY"), @@ -87,6 +87,16 @@ _THIRD_PARTY_PROVIDERS: dict[str, tuple[str | None, str]] = { ), # base_url from CUSTOM_OPENAI_BASE_URL env } +# Providers routed through the Anthropic provider with a custom base_url. +# Maps provider name → (base_url or None, env var for API key). +_ANTHROPIC_ROUTED_PROVIDERS: dict[str, tuple[str | None, str]] = { + "minimax": (_MINIMAX_ANTHROPIC_BASE_URL, "MINIMAX_API_KEY"), + "custom-anthropic": (None, "CUSTOM_ANTHROPIC_API_KEY"), +} + +# Anthropic-routed providers that support extended thinking. +_THINKING_CAPABLE_PROVIDERS: set[str] = {"minimax"} + # Model registry: list of (short_name, model_id, provider) # Allows same short_name across different providers. _MODEL_ENTRIES: list[tuple[str, str, str]] = [ @@ -108,6 +118,8 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [ ("claude-haiku-4-5", "claude-haiku-4-5", "anthropic"), # OpenAI ("gpt-5.4", "gpt-5.4-2026-03-05", "openai"), + ("gpt-5.4-mini", "gpt-5.4-mini", "openai"), + ("gpt-5.4-nano", "gpt-5.4-nano", "openai"), ("gpt-5.3-codex", "gpt-5.3-codex", "openai"), ("gpt-5.2-codex", "gpt-5.2-codex", "openai"), ("gpt-5.2", "gpt-5.2-2025-12-11", "openai"), @@ -127,7 +139,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 (direct API — Anthropic-compatible at api.minimaxi.com) + ("minimax-m2.7", "MiniMax-M2.7", "minimax"), + ("minimax-m2.7-highspeed", "MiniMax-M2.7-highspeed", "minimax"), ("minimax-m2.5", "MiniMax-M2.5", "minimax"), ("minimax-m2.5-highspeed", "MiniMax-M2.5-highspeed", "minimax"), # NVIDIA @@ -204,6 +218,7 @@ def _apply_auto_config( model_id: str, is_third_party: bool, kwargs: dict[str, Any], + original_provider: str | None = None, ) -> None: """Auto-enable provider-specific features (thinking, reasoning, etc.). @@ -212,11 +227,17 @@ def _apply_auto_config( """ # Anthropic: extended thinking if provider == "anthropic" and "thinking" not in kwargs: - base_url = os.environ.get("ANTHROPIC_BASE_URL", "") - _is_proxy = "127.0.0.1" in base_url or "localhost" in base_url - if is_third_party or _is_proxy: - # ccproxy manages thinking internally; don't set it here - # to avoid 422 errors with thinking content blocks in history + _supports_thinking = original_provider in _THINKING_CAPABLE_PROVIDERS + # Only check ANTHROPIC_BASE_URL for proxy detection on native Anthropic + # (routed providers have their own base_url set in kwargs already). + if not is_third_party: + base_url = os.environ.get("ANTHROPIC_BASE_URL", "") + _is_proxy = "127.0.0.1" in base_url or "localhost" in base_url + else: + _is_proxy = False + if _is_proxy or (is_third_party and not _supports_thinking): + # ccproxy / generic third-party: skip thinking to avoid + # 422 errors with thinking content blocks in history pass elif model_id.endswith("4-6"): kwargs["thinking"] = {"type": "adaptive"} @@ -298,8 +319,11 @@ def get_chat_model( provider = "anthropic" # Default fallback # Anthropic base_url override (e.g. ccproxy at localhost:8000/api/v1) - _is_third_party = provider in _THIRD_PARTY_PROVIDERS + _is_third_party = ( + provider in _OPENAI_ROUTED_PROVIDERS or provider in _ANTHROPIC_ROUTED_PROVIDERS + ) _is_openai_proxy = False + _original_provider: str | None = None if provider == "anthropic": base_url = os.environ.get("ANTHROPIC_BASE_URL", "") if base_url: @@ -325,9 +349,9 @@ def get_chat_model( if api_key: kwargs["api_key"] = api_key - # Third-party providers → route through OpenAI provider with base_url - elif provider in _THIRD_PARTY_PROVIDERS: - base_url_default, api_key_env = _THIRD_PARTY_PROVIDERS[provider] + # OpenAI-routed providers → route through OpenAI provider with base_url + elif provider in _OPENAI_ROUTED_PROVIDERS: + base_url_default, api_key_env = _OPENAI_ROUTED_PROVIDERS[provider] if provider == "custom-openai": base_url = os.environ.get("CUSTOM_OPENAI_BASE_URL", "") if not base_url: @@ -349,26 +373,35 @@ def get_chat_model( if provider == "siliconflow": kwargs.setdefault("extra_body", {})["enable_thinking"] = False provider = "openai" - elif provider == "custom-anthropic": - base_url = os.environ.get("CUSTOM_ANTHROPIC_BASE_URL", "") - if not base_url: - raise ValueError( - "CUSTOM_ANTHROPIC_BASE_URL environment variable is required when using " - "the 'custom-anthropic' provider. Please set it to your " - "Anthropic-compatible API endpoint URL (e.g. https://api.anthropic.com)." - ) - kwargs["base_url"] = base_url.rstrip("/") - api_key = os.environ.get("CUSTOM_ANTHROPIC_API_KEY", "") + + # Anthropic-routed providers → route through Anthropic provider with base_url + elif provider in _ANTHROPIC_ROUTED_PROVIDERS: + base_url_default, api_key_env = _ANTHROPIC_ROUTED_PROVIDERS[provider] + if provider == "custom-anthropic": + base_url = os.environ.get("CUSTOM_ANTHROPIC_BASE_URL", "") + if not base_url: + raise ValueError( + "CUSTOM_ANTHROPIC_BASE_URL environment variable is required when using " + "the 'custom-anthropic' provider. Please set it to your " + "Anthropic-compatible API endpoint URL (e.g. https://api.anthropic.com)." + ) + base_url = base_url.rstrip("/") + else: + base_url = base_url_default + if base_url: + kwargs["base_url"] = base_url + api_key = os.environ.get(api_key_env, "") if api_key: kwargs["api_key"] = api_key - _is_third_party = True # skip thinking in _apply_auto_config + _original_provider = provider provider = "anthropic" + elif provider == "ollama": base_url = os.environ.get("OLLAMA_BASE_URL", "") if base_url: kwargs["base_url"] = base_url - _apply_auto_config(provider, model_id, _is_third_party, kwargs) + _apply_auto_config(provider, model_id, _is_third_party, kwargs, _original_provider) chat_model = init_chat_model(model=model_id, model_provider=provider, **kwargs) diff --git a/README.md b/README.md index 89abfea..4209a9e 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ 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 +export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com (Anthropic-compatible) 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 0f26096..55938a9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -239,7 +239,7 @@ 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 +export MINIMAX_API_KEY="sk-..." # MiniMax — platform.minimaxi.com (Anthropic-compatible) export NVIDIA_API_KEY="nvapi-..." # NIM — build.nvidia.com # 网络搜索(可选) diff --git a/tests/test_llm.py b/tests/test_llm.py index 907f236..540f8ff 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -481,27 +481,28 @@ 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.""" + def test_minimax_routes_through_anthropic(self, mock_init, monkeypatch): + """MiniMax provider should route through Anthropic 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["model_provider"] == "anthropic" + 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_no_reasoning(self, mock_init, monkeypatch): - """MiniMax provider should NOT get auto-reasoning (routed via OpenAI).""" + def test_minimax_gets_thinking(self, mock_init, monkeypatch): + """MiniMax provider should get auto-thinking (thinking-capable via Anthropic).""" 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 "thinking" in call_kwargs assert "reasoning" not in call_kwargs @patch("EvoScientist.llm.models.init_chat_model") @@ -514,7 +515,7 @@ class TestThirdPartyRouting: call_kwargs = mock_init.call_args[1] assert call_kwargs["model"] == "MiniMax-M2.5" - assert call_kwargs["model_provider"] == "openai" + assert call_kwargs["model_provider"] == "anthropic" @patch("EvoScientist.llm.models.init_chat_model") def test_minimax_highspeed_model(self, mock_init, monkeypatch): @@ -526,8 +527,24 @@ class TestThirdPartyRouting: 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" + assert call_kwargs["model_provider"] == "anthropic" + assert call_kwargs["base_url"] == "https://api.minimaxi.com/anthropic" + + @patch("EvoScientist.llm.models.init_chat_model") + def test_custom_anthropic_via_routed_dict(self, mock_init, monkeypatch): + """custom-anthropic should work via _ANTHROPIC_ROUTED_PROVIDERS dict.""" + mock_init.return_value = "mock_model" + monkeypatch.setenv("CUSTOM_ANTHROPIC_BASE_URL", "https://my-claude.example.com") + monkeypatch.setenv("CUSTOM_ANTHROPIC_API_KEY", "ca-key-789") + + get_chat_model("claude-sonnet-4-6", provider="custom-anthropic") + + call_kwargs = mock_init.call_args[1] + assert call_kwargs["model_provider"] == "anthropic" + assert call_kwargs["base_url"] == "https://my-claude.example.com" + assert call_kwargs["api_key"] == "ca-key-789" + # custom-anthropic is NOT thinking-capable → thinking skipped + assert "thinking" not in call_kwargs # ============================================================================= @@ -536,20 +553,28 @@ class TestThirdPartyRouting: 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 + def test_minimax_in_anthropic_routed_providers(self): + """MiniMax should be registered in _ANTHROPIC_ROUTED_PROVIDERS.""" + from EvoScientist.llm.models import _ANTHROPIC_ROUTED_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 "minimax" in _ANTHROPIC_ROUTED_PROVIDERS + base_url, api_key_env = _ANTHROPIC_ROUTED_PROVIDERS["minimax"] + assert base_url == "https://api.minimaxi.com/anthropic" assert api_key_env == "MINIMAX_API_KEY" + def test_minimax_not_in_openai_routed_providers(self): + """MiniMax should NOT be in _OPENAI_ROUTED_PROVIDERS (moved to Anthropic).""" + from EvoScientist.llm.models import _OPENAI_ROUTED_PROVIDERS + + assert "minimax" not in _OPENAI_ROUTED_PROVIDERS + def test_minimax_models_registered(self): - """MiniMax should have direct model entries in _MODEL_ENTRIES.""" + """MiniMax should have 4 direct model entries in _MODEL_ENTRIES.""" minimax_models = get_models_for_provider("minimax") - assert len(minimax_models) >= 2 + assert len(minimax_models) == 4 model_names = {name for name, _ in minimax_models} + assert "minimax-m2.7" in model_names + assert "minimax-m2.7-highspeed" in model_names assert "minimax-m2.5" in model_names assert "minimax-m2.5-highspeed" in model_names @@ -557,6 +582,7 @@ class TestMiniMaxProvider: """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.7"] == "MiniMax-M2.7" assert model_dict["minimax-m2.5"] == "MiniMax-M2.5" assert model_dict["minimax-m2.5-highspeed"] == "MiniMax-M2.5-highspeed" diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py index f961d89..fe0aeaf 100644 --- a/tests/test_minimax_integration.py +++ b/tests/test_minimax_integration.py @@ -1,7 +1,7 @@ """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. +MiniMax API (api.minimaxi.com/anthropic) and produce chat completions. Requires MINIMAX_API_KEY environment variable to be set. """ @@ -42,3 +42,10 @@ class TestMiniMaxIntegration: response = model.invoke("What is 2+2? Answer with just the number.") assert response.content assert "4" in response.content + + def test_minimax_m27_chat_completion(self): + """Test that MiniMax M2.7 can produce a chat completion.""" + model = get_chat_model("minimax-m2.7", provider="minimax", temperature=0) + response = model.invoke("Reply with exactly: test") + assert response.content + assert len(response.content) > 0