Feat/minimax anthropic routing (#75)
* feat: update MiniMax integration to use Anthropic-compatible endpoint and enhance routing logic * refactor: streamline OAuth install hint and update ccproxy health check timeout
This commit is contained in:
+13
-11
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+62
-29
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
# 网络搜索(可选)
|
||||
|
||||
+43
-17
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user