From f27546976322876bd3c5eb061d1c5ae0fbb4424e Mon Sep 17 00:00:00 2001 From: octo-patch Date: Wed, 13 May 2026 15:10:27 +0800 Subject: [PATCH] feat: add MiniMax provider support - Add MiniMax (MiniMax-M2.7) to the onboarding wizard as a primary provider option, routed through LiteLLM - Document MINIMAX_API_KEY in .env.example with DEFAULT_MODEL example - Update README to list MiniMax in the supported providers section - Add unit tests (11) covering provider registration, model resolution, and environment variable documentation - Add integration tests (2) verifying chat completion and streaming against the MiniMax OpenAI-compatible API API docs: - Chat (OpenAI Compatible): https://platform.minimax.io/docs/api-reference/text-openai-api - Chat (Anthropic Compatible): https://platform.minimax.io/docs/api-reference/text-anthropic-api --- .env.example | 5 ++ README.md | 1 + onboard.py | 6 ++ tests/__init__.py | 0 tests/test_minimax_integration.py | 74 +++++++++++++++++ tests/test_minimax_provider.py | 127 ++++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_minimax_integration.py create mode 100644 tests/test_minimax_provider.py diff --git a/.env.example b/.env.example index 3c2edae0..75601dc4 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,10 @@ ANTHROPIC_API_KEY= # Google Gemini — set this if using Google as your primary provider. GOOGLE_API_KEY= +# MiniMax — set this if using MiniMax as your primary provider. +# Get your key at https://platform.minimaxi.com +MINIMAX_API_KEY= + # ── Model selection ─────────────────────────── @@ -24,6 +28,7 @@ GOOGLE_API_KEY= # OpenAI example: DEFAULT_MODEL=gpt-5.2 # Anthropic example: DEFAULT_MODEL=litellm/claude-sonnet-4-6 # Google example: DEFAULT_MODEL=litellm/gemini/gemini-3-flash +# MiniMax example: DEFAULT_MODEL=minimax/MiniMax-M2.7 DEFAULT_MODEL= diff --git a/README.md b/README.md index e3babc33..3a01ec53 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ The setup wizard walks you through everything, but you'll need at least one of t - `OPENAI_API_KEY` - For GPT 5.5 and Sora video generation - `ANTHROPIC_API_KEY` - For Claude models +- `MINIMAX_API_KEY` - For MiniMax models (MiniMax-M2.7) **Optional superpowers:** diff --git a/onboard.py b/onboard.py index 3a838b88..860acac8 100644 --- a/onboard.py +++ b/onboard.py @@ -67,6 +67,12 @@ "default_model": "litellm/gemini/gemini-3-flash", "url": "https://aistudio.google.com/app/apikey", }, + { + "name": "MiniMax", + "env_key": "MINIMAX_API_KEY", + "default_model": "minimax/MiniMax-M2.7", + "url": "https://platform.minimaxi.com", + }, ] # ── add-on definitions ──────────────────────────────────────────────────────── diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py new file mode 100644 index 00000000..00c102c5 --- /dev/null +++ b/tests/test_minimax_integration.py @@ -0,0 +1,74 @@ +"""Integration test for MiniMax provider via LiteLLM OpenAI-compatible API. + +Requires MINIMAX_API_KEY to be set. Skipped automatically when the key is absent. +""" + +from __future__ import annotations + +import os + +import pytest + +# Load API key from ~/.env.local if present +try: + from dotenv import load_dotenv + from pathlib import Path + _env_local = Path.home() / ".env.local" + if _env_local.exists(): + load_dotenv(_env_local) +except ImportError: + pass + +_API_KEY = os.getenv("MINIMAX_API_KEY") + +pytestmark = pytest.mark.skipif(not _API_KEY, reason="MINIMAX_API_KEY not set") + + +@pytest.mark.timeout(30) +def test_minimax_chat_completion(): + """Send a basic chat message via the MiniMax OpenAI-compatible API.""" + import httpx + + response = httpx.post( + "https://api.minimax.io/v1/chat/completions", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {_API_KEY}", + }, + json={ + "model": "MiniMax-M2.7", + "messages": [{"role": "user", "content": 'Say "test passed"'}], + "max_tokens": 20, + "temperature": 1.0, + }, + timeout=30.0, + ) + assert response.status_code == 200, f"API returned {response.status_code}: {response.text}" + data = response.json() + assert data["choices"][0]["message"]["content"], "Response content must not be empty" + + +@pytest.mark.timeout(30) +def test_minimax_streaming(): + """Verify streaming works via the MiniMax OpenAI-compatible API.""" + import httpx + + with httpx.stream( + "POST", + "https://api.minimax.io/v1/chat/completions", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {_API_KEY}", + }, + json={ + "model": "MiniMax-M2.7", + "messages": [{"role": "user", "content": "Say hello"}], + "max_tokens": 20, + "temperature": 1.0, + "stream": True, + }, + timeout=30.0, + ) as response: + assert response.status_code == 200 + chunks = list(response.iter_lines()) + assert len(chunks) > 0, "Must receive at least one streaming chunk" diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py new file mode 100644 index 00000000..1e08aabd --- /dev/null +++ b/tests/test_minimax_provider.py @@ -0,0 +1,127 @@ +"""Unit tests for MiniMax provider integration.""" + +from __future__ import annotations + +import os +import importlib +from unittest import mock + +import pytest + + +# --------------------------------------------------------------------------- +# onboard.py – provider registration +# --------------------------------------------------------------------------- + +class TestOnboardProviderRegistration: + """Verify MiniMax appears in the PROVIDERS list with correct metadata.""" + + def _load_providers(self): + import onboard + importlib.reload(onboard) + return onboard.PROVIDERS + + def test_minimax_provider_present(self): + providers = self._load_providers() + names = [p["name"] for p in providers] + assert "MiniMax" in names, "MiniMax must be listed in PROVIDERS" + + def test_minimax_env_key(self): + providers = self._load_providers() + mm = next(p for p in providers if p["name"] == "MiniMax") + assert mm["env_key"] == "MINIMAX_API_KEY" + + def test_minimax_default_model(self): + providers = self._load_providers() + mm = next(p for p in providers if p["name"] == "MiniMax") + assert mm["default_model"] == "minimax/MiniMax-M2.7" + + def test_minimax_url(self): + providers = self._load_providers() + mm = next(p for p in providers if p["name"] == "MiniMax") + assert mm["url"], "URL must be set" + assert "minimax" in mm["url"].lower() or "minimax" in mm["url"].lower() + + +# --------------------------------------------------------------------------- +# config.py – model resolution +# --------------------------------------------------------------------------- + +class TestConfigModelResolution: + """Verify config.py resolves MiniMax model strings correctly.""" + + def test_minimax_model_is_not_openai(self): + """minimax/MiniMax-M2.7 contains '/' so is_openai_provider() should return False.""" + with mock.patch.dict(os.environ, {"DEFAULT_MODEL": "minimax/MiniMax-M2.7"}): + import config + importlib.reload(config) + assert config.is_openai_provider() is False + + def test_minimax_model_resolved_through_litellm(self): + """_resolve should wrap minimax/MiniMax-M2.7 via LitellmModel.""" + import config + importlib.reload(config) + result = config._resolve("minimax/MiniMax-M2.7") + # If agency_swarm is installed, result is a LitellmModel; otherwise + # the bare string is returned as fallback. + try: + from agency_swarm import LitellmModel + assert isinstance(result, LitellmModel) + except ImportError: + assert result == "minimax/MiniMax-M2.7" + + def test_minimax_highspeed_model_resolved(self): + """Highspeed variant should also resolve correctly.""" + import config + importlib.reload(config) + result = config._resolve("minimax/MiniMax-M2.7-highspeed") + try: + from agency_swarm import LitellmModel + assert isinstance(result, LitellmModel) + except ImportError: + assert result == "minimax/MiniMax-M2.7-highspeed" + + def test_get_default_model_with_minimax(self): + """get_default_model() should return resolved MiniMax model when DEFAULT_MODEL is set.""" + with mock.patch.dict(os.environ, {"DEFAULT_MODEL": "minimax/MiniMax-M2.7"}): + import config + importlib.reload(config) + result = config.get_default_model() + # Should not be the raw string (it should be resolved) + try: + from agency_swarm import LitellmModel + assert isinstance(result, LitellmModel) + except ImportError: + assert result == "minimax/MiniMax-M2.7" + + def test_litellm_prefix_stripped_correctly(self): + """'litellm/minimax/MiniMax-M2.7' should strip to 'minimax/MiniMax-M2.7' for LitellmModel.""" + import config + importlib.reload(config) + result = config._resolve("litellm/minimax/MiniMax-M2.7") + try: + from agency_swarm import LitellmModel + assert isinstance(result, LitellmModel) + except ImportError: + # When agency_swarm is not installed, _resolve returns the original string + assert result == "litellm/minimax/MiniMax-M2.7" + + +# --------------------------------------------------------------------------- +# .env.example – documentation +# --------------------------------------------------------------------------- + +class TestEnvExample: + """Verify .env.example documents MINIMAX_API_KEY.""" + + def test_minimax_api_key_documented(self): + from pathlib import Path + env_example = Path(__file__).resolve().parent.parent / ".env.example" + content = env_example.read_text(encoding="utf-8") + assert "MINIMAX_API_KEY" in content, "MINIMAX_API_KEY must be documented in .env.example" + + def test_minimax_default_model_example(self): + from pathlib import Path + env_example = Path(__file__).resolve().parent.parent / ".env.example" + content = env_example.read_text(encoding="utf-8") + assert "minimax/MiniMax-M2.7" in content, "DEFAULT_MODEL example for MiniMax must be in .env.example"