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:
Xi Zhang
2026-03-19 16:01:06 +01:00
committed by GitHub
parent a011dca693
commit 47e5ef6219
8 changed files with 163 additions and 90 deletions
+13 -11
View File
@@ -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)
+10 -10
View File
@@ -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)
+25 -20
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+8 -1
View File
@@ -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