feat: add MiniMax as cloud inference provider with M2.7 default (#85)

* feat: add MiniMax as cloud inference provider

Add MiniMax M2.5 and M2.5-highspeed as a 5th cloud provider alongside
OpenAI, Anthropic, Google, and OpenRouter. Uses the OpenAI-compatible
API at api.minimax.io/v1 via the existing openai SDK dependency.

Changes:
- Add MiniMax client init, generate, and streaming in CloudEngine
- Add MiniMax models to model catalog with correct pricing
- Add MINIMAX_API_KEY environment variable support
- Add temperature clamping (0.01-1.0) per MiniMax API constraints
- Add 19 unit tests and 3 integration tests
- Update docs and README with MiniMax provider info

* feat: upgrade MiniMax default model to M2.7

- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list
- Set MiniMax-M2.7 as default model (first in list)
- Keep all previous models (M2.5, M2.5-highspeed) as alternatives
- Update pricing table with M2.7 entries
- Update model catalog with M2.7 specs
- Update docs to list all available MiniMax models
- Update unit tests (23 passing) and integration tests (3 passing)

---------

Co-authored-by: Octopus <octo-patch@users.noreply.github.com>
This commit is contained in:
Octopus
2026-03-20 04:16:33 -07:00
committed by GitHub
co-authored by Octopus
parent cbdb720e06
commit 62eaa70736
6 changed files with 580 additions and 4 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ uv sync # core framework
uv sync --extra server # + FastAPI server
```
You also need a local inference backend: [Ollama](https://ollama.com), [vLLM](https://github.com/vllm-project/vllm), [SGLang](https://github.com/sgl-project/sglang), or [llama.cpp](https://github.com/ggerganov/llama.cpp).
You also need a local inference backend: [Ollama](https://ollama.com), [vLLM](https://github.com/vllm-project/vllm), [SGLang](https://github.com/sgl-project/sglang), or [llama.cpp](https://github.com/ggerganov/llama.cpp). Alternatively, use the `cloud` engine with [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Google Gemini](https://ai.google.dev), [OpenRouter](https://openrouter.ai), or [MiniMax](https://www.minimax.io) by setting the corresponding API key environment variable.
## Quick Start
+2 -1
View File
@@ -140,7 +140,7 @@ max_tokens = 1024
| `checkpoint_path` | string | `""` | Path to a fine-tuned checkpoint or LoRA adapter directory. |
| `quantization` | string | `"none"` | Quantization format. Accepted values: `none`, `fp8`, `int8`, `int4`, `gguf_q4`, `gguf_q8`. |
| `preferred_engine` | string | `""` | Override engine for this model (e.g., `"vllm"`). Takes priority over `engine.default`. |
| `provider` | string | `""` | Model provider hint: `local`, `openai`, `anthropic`, `google`. Used by the Cloud engine to route API calls. |
| `provider` | string | `""` | Model provider hint: `local`, `openai`, `anthropic`, `google`, `minimax`. Used by the Cloud engine to route API calls. |
**Generation default fields** (overridable per-call):
@@ -972,6 +972,7 @@ OpenJarvis respects the following environment variables:
| `OPENAI_API_KEY` | API key for OpenAI cloud inference. Required for the `cloud` engine with OpenAI models. |
| `ANTHROPIC_API_KEY` | API key for Anthropic cloud inference. Required for the `cloud` engine with Claude models. |
| `GOOGLE_API_KEY` | API key for Google Gemini inference. Required for the `google` engine. |
| `MINIMAX_API_KEY` | API key for MiniMax cloud inference. Required for the `cloud` engine with MiniMax models (MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed). |
| `TAVILY_API_KEY` | API key for the Tavily web search tool. Required for the `web_search` tool. |
## Next Steps
+120 -2
View File
@@ -1,4 +1,4 @@
"""Cloud inference engine — OpenAI, Anthropic, and Google API backends."""
"""Cloud inference engine — OpenAI, Anthropic, Google, and MiniMax API backends."""
from __future__ import annotations
@@ -38,6 +38,10 @@ PRICING: Dict[str, tuple[float, float]] = {
"gemini-3.1-flash-lite-preview": (0.30, 2.50),
"gemini-3-flash-preview": (0.50, 3.00),
"claude-haiku-4-5-20251001": (1.00, 5.00),
"MiniMax-M2.7": (0.30, 1.20),
"MiniMax-M2.7-highspeed": (0.60, 2.40),
"MiniMax-M2.5": (0.30, 1.20),
"MiniMax-M2.5-highspeed": (0.60, 2.40),
}
# Well-known model IDs per provider
@@ -62,6 +66,12 @@ _GOOGLE_MODELS = [
"gemini-3.1-flash-lite-preview",
"gemini-3-flash-preview",
]
_MINIMAX_MODELS = [
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
]
# OpenRouter models — prefixed with "openrouter/" so they can be identified
_OPENROUTER_POPULAR = [
@@ -76,6 +86,10 @@ _OPENROUTER_POPULAR = [
]
def _is_minimax_model(model: str) -> bool:
return model.lower().startswith("minimax")
def _is_openrouter_model(model: str) -> bool:
return model.startswith("openrouter/")
@@ -170,7 +184,7 @@ def _convert_tools_to_google(
@EngineRegistry.register("cloud")
class CloudEngine(InferenceEngine):
"""Cloud inference via OpenAI, Anthropic, and Google SDKs."""
"""Cloud inference via OpenAI, Anthropic, Google, and MiniMax SDKs."""
engine_id = "cloud"
@@ -179,6 +193,7 @@ class CloudEngine(InferenceEngine):
self._anthropic_client: Any = None
self._google_client: Any = None
self._openrouter_client: Any = None
self._minimax_client: Any = None
self._init_clients()
def _init_clients(self) -> None:
@@ -214,6 +229,16 @@ class CloudEngine(InferenceEngine):
)
except ImportError:
pass
minimax_key = os.environ.get("MINIMAX_API_KEY")
if minimax_key:
try:
import openai
self._minimax_client = openai.OpenAI(
base_url="https://api.minimax.io/v1",
api_key=minimax_key,
)
except ImportError:
pass
def _generate_openai(
self,
@@ -626,6 +651,59 @@ class CloudEngine(InferenceEngine):
"ttft": elapsed,
}
def _generate_minimax(
self,
messages: Sequence[Message],
*,
model: str,
temperature: float,
max_tokens: int,
**kwargs: Any,
) -> Dict[str, Any]:
if self._minimax_client is None:
raise EngineConnectionError(
"MiniMax client not available — set MINIMAX_API_KEY"
)
# MiniMax requires temperature in (0.0, 1.0]; clamp zero
temperature = max(temperature, 0.01)
temperature = min(temperature, 1.0)
kwargs.pop("response_format", None)
create_kwargs: Dict[str, Any] = {
"model": model,
"messages": messages_to_dicts(messages),
"max_tokens": max_tokens,
"temperature": temperature,
}
t0 = time.monotonic()
resp = self._minimax_client.chat.completions.create(**create_kwargs)
elapsed = time.monotonic() - t0
choice = resp.choices[0]
usage = resp.usage
prompt_tokens = usage.prompt_tokens if usage else 0
completion_tokens = usage.completion_tokens if usage else 0
result: Dict[str, Any] = {
"content": choice.message.content or "",
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": (usage.total_tokens if usage else 0),
},
"model": resp.model,
"finish_reason": choice.finish_reason or "stop",
"cost_usd": estimate_cost(model, prompt_tokens, completion_tokens),
"ttft": elapsed,
}
if hasattr(choice.message, "tool_calls") and choice.message.tool_calls:
result["tool_calls"] = [
{
"id": tc.id,
"name": tc.function.name,
"arguments": tc.function.arguments,
}
for tc in choice.message.tool_calls
]
return result
def generate(
self,
messages: Sequence[Message],
@@ -643,6 +721,8 @@ class CloudEngine(InferenceEngine):
)
if _is_openrouter_model(model):
return self._generate_openrouter(messages, **kw)
if _is_minimax_model(model):
return self._generate_minimax(messages, **kw)
if _is_anthropic_model(model):
return self._generate_anthropic(messages, **kw)
if _is_google_model(model):
@@ -669,6 +749,11 @@ class CloudEngine(InferenceEngine):
messages, **kw
):
yield token
elif _is_minimax_model(model):
async for token in self._stream_minimax(
messages, **kw
):
yield token
elif _is_anthropic_model(model):
async for token in self._stream_anthropic(
messages, **kw
@@ -804,6 +889,32 @@ class CloudEngine(InferenceEngine):
if delta and delta.content:
yield delta.content
async def _stream_minimax(
self,
messages: Sequence[Message],
*,
model: str,
temperature: float,
max_tokens: int,
**kwargs: Any,
) -> AsyncIterator[str]:
if self._minimax_client is None:
raise EngineConnectionError("MiniMax client not available")
temperature = max(temperature, 0.01)
temperature = min(temperature, 1.0)
create_kwargs: Dict[str, Any] = {
"model": model,
"messages": messages_to_dicts(messages),
"max_tokens": max_tokens,
"temperature": temperature,
"stream": True,
}
resp = self._minimax_client.chat.completions.create(**create_kwargs)
for chunk in resp:
delta = chunk.choices[0].delta if chunk.choices else None
if delta and delta.content:
yield delta.content
def list_models(self) -> List[str]:
models: List[str] = []
if self._openai_client is not None:
@@ -814,6 +925,8 @@ class CloudEngine(InferenceEngine):
models.extend(_GOOGLE_MODELS)
if self._openrouter_client is not None:
models.extend(_OPENROUTER_POPULAR)
if self._minimax_client is not None:
models.extend(_MINIMAX_MODELS)
return models
def health(self) -> bool:
@@ -822,6 +935,7 @@ class CloudEngine(InferenceEngine):
or self._anthropic_client is not None
or self._google_client is not None
or self._openrouter_client is not None
or self._minimax_client is not None
)
def close(self) -> None:
@@ -839,6 +953,10 @@ class CloudEngine(InferenceEngine):
if hasattr(self._openrouter_client, "close"):
self._openrouter_client.close()
self._openrouter_client = None
if self._minimax_client is not None:
if hasattr(self._minimax_client, "close"):
self._minimax_client.close()
self._minimax_client = None
__all__ = ["CloudEngine", "PRICING", "_annotate_anthropic_cache", "estimate_cost"]
@@ -707,6 +707,69 @@ BUILTIN_MODELS: List[ModelSpec] = [
},
),
# -----------------------------------------------------------------------
# Cloud models — MiniMax
# -----------------------------------------------------------------------
ModelSpec(
model_id="MiniMax-M2.7",
name="MiniMax M2.7",
parameter_count_b=0.0,
context_length=204800,
supported_engines=("cloud",),
provider="minimax",
requires_api_key=True,
metadata={
"architecture": "proprietary",
"pricing_input": 0.30,
"pricing_output": 1.20,
"url": "https://www.minimax.io",
},
),
ModelSpec(
model_id="MiniMax-M2.7-highspeed",
name="MiniMax M2.7 Highspeed",
parameter_count_b=0.0,
context_length=204800,
supported_engines=("cloud",),
provider="minimax",
requires_api_key=True,
metadata={
"architecture": "proprietary",
"pricing_input": 0.60,
"pricing_output": 2.40,
"url": "https://www.minimax.io",
},
),
ModelSpec(
model_id="MiniMax-M2.5",
name="MiniMax M2.5",
parameter_count_b=0.0,
context_length=204800,
supported_engines=("cloud",),
provider="minimax",
requires_api_key=True,
metadata={
"architecture": "proprietary",
"pricing_input": 0.30,
"pricing_output": 1.20,
"url": "https://www.minimax.io",
},
),
ModelSpec(
model_id="MiniMax-M2.5-highspeed",
name="MiniMax M2.5 Highspeed",
parameter_count_b=0.0,
context_length=204800,
supported_engines=("cloud",),
provider="minimax",
requires_api_key=True,
metadata={
"architecture": "proprietary",
"pricing_input": 0.60,
"pricing_output": 2.40,
"url": "https://www.minimax.io",
},
),
# -----------------------------------------------------------------------
# Cloud models — Google
# -----------------------------------------------------------------------
ModelSpec(
+331
View File
@@ -0,0 +1,331 @@
"""Tests for MiniMax cloud provider support."""
from __future__ import annotations
from types import SimpleNamespace
from unittest import mock
import pytest
from openjarvis.core.registry import EngineRegistry
from openjarvis.core.types import Message, Role
from openjarvis.engine._base import EngineConnectionError
from openjarvis.engine.cloud import (
_MINIMAX_MODELS,
PRICING,
CloudEngine,
_is_minimax_model,
estimate_cost,
)
def _make_cloud_engine(monkeypatch: pytest.MonkeyPatch) -> CloudEngine:
"""Create a CloudEngine with all API keys cleared."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("MINIMAX_API_KEY", raising=False)
if not EngineRegistry.contains("cloud"):
EngineRegistry.register_value("cloud", CloudEngine)
return CloudEngine()
def _fake_minimax_response(
content: str = "Hello from MiniMax!",
model: str = "MiniMax-M2.5",
prompt_tokens: int = 10,
completion_tokens: int = 5,
tool_calls: list | None = None,
) -> SimpleNamespace:
usage = SimpleNamespace(
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=prompt_tokens + completion_tokens,
)
message = SimpleNamespace(content=content, tool_calls=tool_calls)
choice = SimpleNamespace(message=message, finish_reason="stop")
return SimpleNamespace(choices=[choice], usage=usage, model=model)
# ---------------------------------------------------------------------------
# Routing tests
# ---------------------------------------------------------------------------
class TestMiniMaxRouting:
def test_is_minimax_model(self) -> None:
assert _is_minimax_model("MiniMax-M2.7") is True
assert _is_minimax_model("MiniMax-M2.7-highspeed") is True
assert _is_minimax_model("MiniMax-M2.5") is True
assert _is_minimax_model("MiniMax-M2.5-highspeed") is True
assert _is_minimax_model("minimax-m2.7") is True
assert _is_minimax_model("gpt-4o") is False
assert _is_minimax_model("claude-opus-4-6") is False
assert _is_minimax_model("gemini-3-pro") is False
def test_minimax_models_list(self) -> None:
assert "MiniMax-M2.7" in _MINIMAX_MODELS
assert "MiniMax-M2.7-highspeed" in _MINIMAX_MODELS
assert "MiniMax-M2.5" in _MINIMAX_MODELS
assert "MiniMax-M2.5-highspeed" in _MINIMAX_MODELS
def test_m27_is_first_in_list(self) -> None:
assert _MINIMAX_MODELS[0] == "MiniMax-M2.7"
assert _MINIMAX_MODELS[1] == "MiniMax-M2.7-highspeed"
# ---------------------------------------------------------------------------
# Init tests
# ---------------------------------------------------------------------------
class TestMiniMaxInit:
def test_init_with_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key")
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
fake_openai = mock.MagicMock()
with mock.patch.dict("sys.modules", {"openai": fake_openai}):
if not EngineRegistry.contains("cloud"):
EngineRegistry.register_value("cloud", CloudEngine)
engine = CloudEngine()
assert engine._minimax_client is not None
def test_health_with_minimax_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key")
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
fake_openai = mock.MagicMock()
with mock.patch.dict("sys.modules", {"openai": fake_openai}):
engine = CloudEngine()
assert engine.health() is True
def test_no_minimax_key_no_client(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
assert engine._minimax_client is None
# ---------------------------------------------------------------------------
# Generate tests
# ---------------------------------------------------------------------------
class TestMiniMaxGenerate:
def test_m27_generate(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response(
content="I am MiniMax M2.7", model="MiniMax-M2.7"
)
engine._minimax_client = fake_client
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="MiniMax-M2.7"
)
assert result["content"] == "I am MiniMax M2.7"
assert result["model"] == "MiniMax-M2.7"
assert result["usage"]["prompt_tokens"] == 10
assert result["usage"]["completion_tokens"] == 5
def test_m27_highspeed_generate(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response(
content="I am MiniMax M2.7 Highspeed", model="MiniMax-M2.7-highspeed"
)
engine._minimax_client = fake_client
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="MiniMax-M2.7-highspeed"
)
assert result["content"] == "I am MiniMax M2.7 Highspeed"
assert result["model"] == "MiniMax-M2.7-highspeed"
def test_m25_generate(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response(
content="I am MiniMax M2.5", model="MiniMax-M2.5"
)
engine._minimax_client = fake_client
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="MiniMax-M2.5"
)
assert result["content"] == "I am MiniMax M2.5"
assert result["model"] == "MiniMax-M2.5"
assert result["usage"]["prompt_tokens"] == 10
assert result["usage"]["completion_tokens"] == 5
def test_m25_highspeed_generate(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response(
content="I am MiniMax M2.5 Highspeed", model="MiniMax-M2.5-highspeed"
)
engine._minimax_client = fake_client
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="MiniMax-M2.5-highspeed"
)
assert result["content"] == "I am MiniMax M2.5 Highspeed"
assert result["model"] == "MiniMax-M2.5-highspeed"
def test_temperature_clamped_above_zero(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""MiniMax requires temperature in (0.0, 1.0]; verify zero is clamped."""
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response()
engine._minimax_client = fake_client
engine.generate(
[Message(role=Role.USER, content="Hi")],
model="MiniMax-M2.5",
temperature=0.0,
)
call_kwargs = fake_client.chat.completions.create.call_args
actual_temp = call_kwargs.kwargs.get("temperature") or call_kwargs[1].get(
"temperature"
)
assert actual_temp >= 0.01, "Temperature should be clamped above zero"
def test_temperature_clamped_at_max(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""MiniMax requires temperature <= 1.0; verify high values are clamped."""
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response()
engine._minimax_client = fake_client
engine.generate(
[Message(role=Role.USER, content="Hi")],
model="MiniMax-M2.5",
temperature=2.0,
)
call_kwargs = fake_client.chat.completions.create.call_args
actual_temp = call_kwargs.kwargs.get("temperature") or call_kwargs[1].get(
"temperature"
)
assert actual_temp <= 1.0, "Temperature should be clamped at 1.0"
def test_tool_calls_extraction(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_tool_call = SimpleNamespace(
id="call_minimax_123",
type="function",
function=SimpleNamespace(name="search", arguments='{"q":"test"}'),
)
fake_resp = _fake_minimax_response(content="", model="MiniMax-M2.5")
fake_resp.choices[0].message.tool_calls = [fake_tool_call]
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = fake_resp
engine._minimax_client = fake_client
result = engine.generate(
[Message(role=Role.USER, content="Search")], model="MiniMax-M2.5"
)
assert "tool_calls" in result
assert len(result["tool_calls"]) == 1
tc = result["tool_calls"][0]
assert tc["id"] == "call_minimax_123"
assert tc["name"] == "search"
def test_no_tool_calls_when_absent(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
fake_client.chat.completions.create.return_value = _fake_minimax_response(
content="Just text"
)
engine._minimax_client = fake_client
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="MiniMax-M2.5"
)
assert "tool_calls" not in result
def test_no_client_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
assert engine._minimax_client is None
with pytest.raises(
EngineConnectionError, match="MiniMax client not available"
):
engine.generate(
[Message(role=Role.USER, content="Hi")], model="MiniMax-M2.5"
)
# ---------------------------------------------------------------------------
# Model discovery tests
# ---------------------------------------------------------------------------
class TestMiniMaxModelDiscovery:
def test_list_models_includes_minimax(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
engine = _make_cloud_engine(monkeypatch)
engine._minimax_client = mock.MagicMock()
models = engine.list_models()
for m in _MINIMAX_MODELS:
assert m in models
def test_only_minimax_client(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
engine._minimax_client = mock.MagicMock()
models = engine.list_models()
assert set(models) == set(_MINIMAX_MODELS)
# ---------------------------------------------------------------------------
# Pricing tests
# ---------------------------------------------------------------------------
class TestMiniMaxPricing:
def test_minimax_models_in_pricing(self) -> None:
assert "MiniMax-M2.7" in PRICING
assert "MiniMax-M2.7-highspeed" in PRICING
assert "MiniMax-M2.5" in PRICING
assert "MiniMax-M2.5-highspeed" in PRICING
def test_minimax_m27_cost_estimate(self) -> None:
# MiniMax-M2.7: $0.30/M in, $1.20/M out
cost = estimate_cost("MiniMax-M2.7", 1_000_000, 1_000_000)
assert cost == pytest.approx(1.50)
def test_minimax_m27_highspeed_cost_estimate(self) -> None:
# MiniMax-M2.7-highspeed: $0.60/M in, $2.40/M out
cost = estimate_cost("MiniMax-M2.7-highspeed", 1_000_000, 1_000_000)
assert cost == pytest.approx(3.00)
def test_minimax_m25_cost_estimate(self) -> None:
# MiniMax-M2.5: $0.30/M in, $1.20/M out
cost = estimate_cost("MiniMax-M2.5", 1_000_000, 1_000_000)
assert cost == pytest.approx(1.50)
def test_zero_tokens_zero_cost(self) -> None:
assert estimate_cost("MiniMax-M2.7", 0, 0) == 0.0
# ---------------------------------------------------------------------------
# Close tests
# ---------------------------------------------------------------------------
class TestMiniMaxClose:
def test_close_minimax_client(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine = _make_cloud_engine(monkeypatch)
fake_client = mock.MagicMock()
engine._minimax_client = fake_client
engine.close()
assert engine._minimax_client is None
fake_client.close.assert_called_once()
+63
View File
@@ -0,0 +1,63 @@
"""Integration test for MiniMax cloud provider.
Requires MINIMAX_API_KEY environment variable to be set.
Run with: pytest tests/integration/test_minimax_cloud.py -v
"""
from __future__ import annotations
import os
import pytest
from openjarvis.core.registry import EngineRegistry
from openjarvis.core.types import Message, Role
from openjarvis.engine.cloud import CloudEngine
_MINIMAX_KEY = os.environ.get("MINIMAX_API_KEY", "")
_skip_no_key = pytest.mark.skipif(
not _MINIMAX_KEY,
reason="MINIMAX_API_KEY not set",
)
@_skip_no_key
class TestMiniMaxCloudIntegration:
"""Live integration tests against MiniMax Cloud API."""
@pytest.fixture()
def engine(self, monkeypatch: pytest.MonkeyPatch) -> CloudEngine:
monkeypatch.setenv("MINIMAX_API_KEY", _MINIMAX_KEY)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
if not EngineRegistry.contains("cloud"):
EngineRegistry.register_value("cloud", CloudEngine)
return CloudEngine()
def test_m27_highspeed_basic_chat(self, engine: CloudEngine) -> None:
"""Send a simple message via M2.7-highspeed and verify non-empty response."""
result = engine.generate(
[Message(role=Role.USER, content="Reply with exactly: hello world")],
model="MiniMax-M2.7-highspeed",
temperature=0.01,
max_tokens=32,
)
assert result["content"], "Expected non-empty content"
assert result["usage"]["prompt_tokens"] > 0
assert result["usage"]["completion_tokens"] > 0
assert result["finish_reason"] in ("stop", "length")
def test_m27_highspeed_health(self, engine: CloudEngine) -> None:
"""Engine health should be True when MINIMAX_API_KEY is set."""
assert engine.health() is True
def test_m27_highspeed_list_models(self, engine: CloudEngine) -> None:
"""MiniMax models should appear in list_models."""
models = engine.list_models()
assert "MiniMax-M2.7" in models
assert "MiniMax-M2.7-highspeed" in models
assert "MiniMax-M2.5" in models
assert "MiniMax-M2.5-highspeed" in models