feat: add MiniMax as first-class LLM provider

Add MiniMaxProvider with built-in support for MiniMax's OpenAI-compatible
API (MiniMax-M2.7, MiniMax-M2.5, MiniMax-M2.5-highspeed models).

Changes:
- Add MiniMaxProvider class implementing ModelProvider interface
- Add MiniMax model routing in OpenAIChatCompletionsModel for litellm
- Export MiniMaxProvider from agents __init__.py
- Add provider documentation (docs/providers/minimax.md)
- Add usage example (examples/model_providers/minimax_example.py)
- Add MiniMax to README provider list
- Add 18 unit tests and 3 integration tests
This commit is contained in:
PR Bot 2026-03-20 19:32:23 +08:00
parent e22a1220f7
commit a2c1fd60aa
8 changed files with 559 additions and 0 deletions

View File

@ -276,6 +276,7 @@ CAI is built on the following core principles:
- **Anthropic**: `Claude 3.7`, `Claude 3.5`, `Claude 3`, `Claude 3 Opus`
- **OpenAI**: `O1`, `O1 Mini`, `O3 Mini`, `GPT-4o`, `GPT-4.5 Preview`
- **DeepSeek**: `DeepSeek V3`, `DeepSeek R1`
- **MiniMax**: `MiniMax-M2.7`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed` ([docs](docs/providers/minimax.md))
- **Ollama**: `Qwen2.5 72B`, `Qwen2.5 14B`, etc

79
docs/providers/minimax.md Normal file
View File

@ -0,0 +1,79 @@
# MiniMax Configuration
#### [MiniMax AI](https://www.minimax.io/)
MiniMax provides large language models with an OpenAI-compatible API. CAI includes a built-in `MiniMaxProvider` for easy integration.
## Setup
1. Get an API key from [MiniMax](https://platform.minimax.chat/).
2. Set the environment variable:
```bash
export MINIMAX_API_KEY=<your-api-key>
```
## Usage
### Option 1: Using MiniMaxProvider (Recommended)
```python
from cai.sdk.agents import Agent, Runner, RunConfig
from cai.sdk.agents.models.minimax_provider import MiniMaxProvider
provider = MiniMaxProvider()
agent = Agent(
name="assistant",
instructions="You are a helpful assistant.",
)
result = await Runner.run(
agent,
"Hello!",
run_config=RunConfig(model_provider=provider),
)
```
### Option 2: Using environment variables with LiteLLM
```bash
CAI_MODEL=openai/MiniMax-M2.7
MINIMAX_API_KEY=<your-api-key>
OPENAI_API_BASE=https://api.minimax.io/v1
```
### Option 3: Direct model on Agent
```python
from openai import AsyncOpenAI
from cai.sdk.agents import Agent, OpenAIChatCompletionsModel
client = AsyncOpenAI(
api_key="<your-api-key>",
base_url="https://api.minimax.io/v1",
)
agent = Agent(
name="assistant",
instructions="You are a helpful assistant.",
model=OpenAIChatCompletionsModel(
model="MiniMax-M2.7",
openai_client=client,
),
)
```
## Available Models
| Model | Context Window | Description |
|-------|---------------|-------------|
| `MiniMax-M2.7` | 1M tokens | Latest and most capable model |
| `MiniMax-M2.5` | 1M tokens | Strong general-purpose model |
| `MiniMax-M2.5-highspeed` | 204K tokens | Optimized for speed |
## Notes
- MiniMax uses an OpenAI-compatible API, so it works with `OpenAIChatCompletionsModel`.
- Tracing is sent to OpenAI by default. If you don't have an OpenAI API key, disable tracing with `set_tracing_disabled(True)` or set a tracing-specific key.
- Temperature range: `[0.0, 1.0]`.

View File

@ -0,0 +1,69 @@
"""Example of using MiniMax as an LLM provider with CAI.
Prerequisites:
- Set the MINIMAX_API_KEY environment variable.
Usage:
python examples/model_providers/minimax_example.py
"""
from __future__ import annotations
import asyncio
import os
from agents import (
Agent,
RunConfig,
Runner,
function_tool,
set_tracing_disabled,
)
from agents.models.minimax_provider import MiniMaxProvider
# Disable tracing if you don't have an OpenAI API key for trace uploads
set_tracing_disabled(disabled=True)
# Create the MiniMax provider (reads MINIMAX_API_KEY from env)
minimax_provider = MiniMaxProvider()
@function_tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny and 22°C."
async def main():
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant. Keep answers concise.",
tools=[get_weather],
)
# Use MiniMax-M2.7 (default)
result = await Runner.run(
agent,
"What's the weather in Tokyo?",
run_config=RunConfig(model_provider=minimax_provider),
)
print("MiniMax-M2.7 response:", result.final_output)
# Use MiniMax-M2.5-highspeed for faster responses
highspeed_provider = MiniMaxProvider()
agent_fast = Agent(
name="Fast Assistant",
instructions="You are a helpful assistant. Keep answers concise.",
model="MiniMax-M2.5-highspeed",
)
result = await Runner.run(
agent_fast,
"Tell me a short joke.",
run_config=RunConfig(model_provider=highspeed_provider),
)
print("MiniMax-M2.5-highspeed response:", result.final_output)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -42,6 +42,7 @@ from .lifecycle import AgentHooks, RunHooks
from .model_settings import ModelSettings
from .models.interface import Model, ModelProvider, ModelTracing
from .models.openai_chatcompletions import OpenAIChatCompletionsModel
from .models.minimax_provider import MiniMaxProvider
from .models.openai_provider import OpenAIProvider
from .models.openai_responses import OpenAIResponsesModel
from .result import RunResult, RunResultStreaming
@ -153,6 +154,7 @@ __all__ = [
"ModelProvider",
"ModelTracing",
"ModelSettings",
"MiniMaxProvider",
"OpenAIChatCompletionsModel",
"OpenAIProvider",
"OpenAIResponsesModel",

View File

@ -0,0 +1,95 @@
from __future__ import annotations
import os
import httpx
from openai import AsyncOpenAI, DefaultAsyncHttpxClient
from .interface import Model, ModelProvider
from .openai_chatcompletions import OpenAIChatCompletionsModel
MINIMAX_DEFAULT_MODEL: str = "MiniMax-M2.7"
MINIMAX_API_BASE_URL: str = "https://api.minimax.io/v1"
_http_client: httpx.AsyncClient | None = None
def _shared_http_client() -> httpx.AsyncClient:
global _http_client
if _http_client is None:
_http_client = DefaultAsyncHttpxClient()
return _http_client
class MiniMaxProvider(ModelProvider):
"""A model provider that uses MiniMax's OpenAI-compatible API.
MiniMax provides large language models accessible via an OpenAI-compatible
chat completions endpoint. Supported models include MiniMax-M2.7,
MiniMax-M2.5, and MiniMax-M2.5-highspeed.
The provider reads the API key from the ``MINIMAX_API_KEY`` environment
variable by default, or you can pass it explicitly.
Example usage::
from cai.sdk.agents import Agent, Runner, RunConfig
from cai.sdk.agents.models.minimax_provider import MiniMaxProvider
provider = MiniMaxProvider()
agent = Agent(name="assistant", instructions="You are helpful.")
result = await Runner.run(
agent,
"Hello!",
run_config=RunConfig(model_provider=provider),
)
"""
def __init__(
self,
*,
api_key: str | None = None,
base_url: str | None = None,
openai_client: AsyncOpenAI | None = None,
) -> None:
"""Create a new MiniMax provider.
Args:
api_key: The MiniMax API key. If not provided, reads from the
``MINIMAX_API_KEY`` environment variable.
base_url: The base URL for the MiniMax API. Defaults to
``https://api.minimax.io/v1``.
openai_client: An optional pre-configured AsyncOpenAI client.
If provided, ``api_key`` and ``base_url`` are ignored.
"""
if openai_client is not None:
assert api_key is None and base_url is None, (
"Don't provide api_key or base_url if you provide openai_client"
)
self._client: AsyncOpenAI | None = openai_client
else:
self._client = None
self._stored_api_key = api_key
self._stored_base_url = base_url
def _get_client(self) -> AsyncOpenAI:
if self._client is None:
api_key = self._stored_api_key or os.environ.get("MINIMAX_API_KEY")
if not api_key:
raise ValueError(
"MiniMax API key not found. Set the MINIMAX_API_KEY environment "
"variable or pass api_key to MiniMaxProvider()."
)
self._client = AsyncOpenAI(
api_key=api_key,
base_url=self._stored_base_url or MINIMAX_API_BASE_URL,
http_client=_shared_http_client(),
)
return self._client
def get_model(self, model_name: str | None) -> Model:
if model_name is None:
model_name = MINIMAX_DEFAULT_MODEL
client = self._get_client()
return OpenAIChatCompletionsModel(model=model_name, openai_client=client)

View File

@ -2830,6 +2830,16 @@ class OpenAIChatCompletionsModel(Model):
)
elif "gemini" in model_str:
kwargs.pop("parallel_tool_calls", None)
elif "minimax" in model_str:
# MiniMax uses OpenAI-compatible API at api.minimax.io
litellm.drop_params = True
kwargs["api_base"] = str(self._get_client().base_url).rstrip("/")
kwargs["custom_llm_provider"] = "openai"
kwargs["api_key"] = self._get_client().api_key
kwargs.pop("store", None)
kwargs.pop("parallel_tool_calls", None)
if not converted_tools:
kwargs.pop("tool_choice", None)
elif "qwen" in model_str or ":" in model_str:
# Handle Ollama-served models with custom formats (e.g., alias1)
# These typically need the Ollama provider

View File

@ -0,0 +1,197 @@
"""Unit tests for MiniMaxProvider."""
from __future__ import annotations
import os
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai import AsyncOpenAI
from cai.sdk.agents import Agent, RunConfig, Runner
from cai.sdk.agents.models.interface import Model, ModelProvider
from cai.sdk.agents.models.minimax_provider import (
MINIMAX_API_BASE_URL,
MINIMAX_DEFAULT_MODEL,
MiniMaxProvider,
)
from cai.sdk.agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
from tests.fake_model import FakeModel
from tests.core.test_responses import get_text_message
class TestMiniMaxProviderInit:
"""Tests for MiniMaxProvider initialization."""
def test_default_init_without_api_key_raises(self) -> None:
"""Creating a provider without an API key and without env var should
raise ValueError on first use."""
provider = MiniMaxProvider()
with patch.dict(os.environ, {}, clear=True):
# Remove MINIMAX_API_KEY if set
env = os.environ.copy()
env.pop("MINIMAX_API_KEY", None)
with patch.dict(os.environ, env, clear=True):
with pytest.raises(ValueError, match="MiniMax API key not found"):
provider._get_client()
def test_init_with_api_key(self) -> None:
"""Provider should accept an explicit API key."""
provider = MiniMaxProvider(api_key="test-key-123")
client = provider._get_client()
assert isinstance(client, AsyncOpenAI)
assert client.api_key == "test-key-123"
def test_init_with_base_url(self) -> None:
"""Provider should accept a custom base URL."""
provider = MiniMaxProvider(
api_key="test-key",
base_url="https://custom.minimax.io/v1",
)
client = provider._get_client()
assert str(client.base_url).rstrip("/") == "https://custom.minimax.io/v1"
def test_init_with_openai_client(self) -> None:
"""Provider should accept a pre-configured AsyncOpenAI client."""
custom_client = AsyncOpenAI(
api_key="custom-key",
base_url="https://api.minimax.io/v1",
)
provider = MiniMaxProvider(openai_client=custom_client)
assert provider._get_client() is custom_client
def test_init_with_client_and_key_raises(self) -> None:
"""Providing both openai_client and api_key should raise."""
custom_client = AsyncOpenAI(
api_key="custom-key",
base_url="https://api.minimax.io/v1",
)
with pytest.raises(AssertionError):
MiniMaxProvider(openai_client=custom_client, api_key="extra-key")
def test_init_with_env_api_key(self) -> None:
"""Provider should read from MINIMAX_API_KEY env var."""
with patch.dict(os.environ, {"MINIMAX_API_KEY": "env-key-456"}):
provider = MiniMaxProvider()
client = provider._get_client()
assert client.api_key == "env-key-456"
def test_default_base_url(self) -> None:
"""Provider should default to MiniMax API base URL."""
provider = MiniMaxProvider(api_key="test-key")
client = provider._get_client()
assert str(client.base_url).rstrip("/") == MINIMAX_API_BASE_URL
class TestMiniMaxProviderGetModel:
"""Tests for MiniMaxProvider.get_model()."""
def test_get_model_returns_chat_completions_model(self) -> None:
"""get_model should return an OpenAIChatCompletionsModel."""
provider = MiniMaxProvider(api_key="test-key")
model = provider.get_model("MiniMax-M2.7")
assert isinstance(model, OpenAIChatCompletionsModel)
def test_get_model_with_none_uses_default(self) -> None:
"""get_model(None) should use the default MiniMax model name."""
provider = MiniMaxProvider(api_key="test-key")
model = provider.get_model(None)
assert isinstance(model, OpenAIChatCompletionsModel)
assert model.model == MINIMAX_DEFAULT_MODEL
def test_get_model_with_custom_name(self) -> None:
"""get_model should pass through a custom model name."""
provider = MiniMaxProvider(api_key="test-key")
model = provider.get_model("MiniMax-M2.5-highspeed")
assert isinstance(model, OpenAIChatCompletionsModel)
assert model.model == "MiniMax-M2.5-highspeed"
def test_get_model_m2_5(self) -> None:
"""get_model should work with MiniMax-M2.5."""
provider = MiniMaxProvider(api_key="test-key")
model = provider.get_model("MiniMax-M2.5")
assert isinstance(model, OpenAIChatCompletionsModel)
assert model.model == "MiniMax-M2.5"
def test_lazy_client_initialization(self) -> None:
"""Client should not be created until get_model is called."""
provider = MiniMaxProvider(api_key="test-key")
assert provider._client is None
provider.get_model("MiniMax-M2.7")
assert provider._client is not None
class TestMiniMaxProviderIsModelProvider:
"""Tests that MiniMaxProvider implements ModelProvider correctly."""
def test_is_model_provider(self) -> None:
"""MiniMaxProvider should be an instance of ModelProvider."""
provider = MiniMaxProvider(api_key="test-key")
assert isinstance(provider, ModelProvider)
def test_get_model_returns_model(self) -> None:
"""get_model should return an instance of Model."""
provider = MiniMaxProvider(api_key="test-key")
model = provider.get_model("MiniMax-M2.7")
assert isinstance(model, Model)
class TestMiniMaxProviderWithRunner:
"""Tests for MiniMaxProvider integration with Runner."""
@pytest.mark.asyncio
async def test_run_config_with_minimax_provider(self) -> None:
"""MiniMaxProvider should work with RunConfig and Runner."""
fake_model = FakeModel(initial_output=[get_text_message("minimax-response")])
class MockMiniMaxProvider(ModelProvider):
def __init__(self) -> None:
self.last_requested: str | None = None
def get_model(self, model_name: str | None) -> Model:
self.last_requested = model_name
return fake_model
provider = MockMiniMaxProvider()
agent = Agent(name="test", model="MiniMax-M2.7")
run_config = RunConfig(model_provider=provider)
result = await Runner.run(agent, input="Hello", run_config=run_config)
assert provider.last_requested == "MiniMax-M2.7"
assert result.final_output == "minimax-response"
@pytest.mark.asyncio
async def test_run_config_model_override(self) -> None:
"""Model name override in RunConfig should work with MiniMax provider."""
fake_model = FakeModel(
initial_output=[get_text_message("highspeed-response")]
)
class MockMiniMaxProvider(ModelProvider):
def __init__(self) -> None:
self.last_requested: str | None = None
def get_model(self, model_name: str | None) -> Model:
self.last_requested = model_name
return fake_model
provider = MockMiniMaxProvider()
agent = Agent(name="test", model="MiniMax-M2.7")
run_config = RunConfig(
model="MiniMax-M2.5-highspeed", model_provider=provider
)
result = await Runner.run(agent, input="Hello", run_config=run_config)
assert provider.last_requested == "MiniMax-M2.5-highspeed"
assert result.final_output == "highspeed-response"
class TestMiniMaxProviderConstants:
"""Tests for MiniMax provider constants."""
def test_default_model(self) -> None:
assert MINIMAX_DEFAULT_MODEL == "MiniMax-M2.7"
def test_api_base_url(self) -> None:
assert MINIMAX_API_BASE_URL == "https://api.minimax.io/v1"

View File

@ -0,0 +1,106 @@
"""Integration tests for MiniMax provider.
These tests require a valid MINIMAX_API_KEY environment variable and network
access to the MiniMax API.
Run with:
MINIMAX_API_KEY=<key> pytest tests/integration/test_minimax_integration.py -v
"""
from __future__ import annotations
import os
import pytest
from cai.sdk.agents import (
Agent,
ModelSettings,
RunConfig,
Runner,
set_tracing_disabled,
)
from cai.sdk.agents.models.minimax_provider import MiniMaxProvider
# Disable tracing for integration tests (no OpenAI key needed)
set_tracing_disabled(disabled=True)
MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY", "")
pytestmark = [
pytest.mark.skipif(
not MINIMAX_API_KEY,
reason="MINIMAX_API_KEY not set",
),
pytest.mark.allow_call_model_methods,
]
@pytest.fixture
def minimax_provider() -> MiniMaxProvider:
return MiniMaxProvider(api_key=MINIMAX_API_KEY)
@pytest.mark.asyncio
@pytest.mark.timeout(120)
async def test_minimax_m27_basic_response(minimax_provider: MiniMaxProvider) -> None:
"""MiniMax-M2.7 should return a basic text response."""
set_tracing_disabled(disabled=True)
agent = Agent(
name="test",
instructions="Respond with exactly: HELLO",
model="MiniMax-M2.7",
)
result = await Runner.run(
agent,
"Say hello",
run_config=RunConfig(
model_provider=minimax_provider,
model_settings=ModelSettings(temperature=0.01, max_tokens=50),
),
)
assert result.final_output is not None
assert len(result.final_output) > 0
@pytest.mark.asyncio
@pytest.mark.timeout(120)
async def test_minimax_m25_highspeed(minimax_provider: MiniMaxProvider) -> None:
"""MiniMax-M2.5-highspeed should return a response."""
set_tracing_disabled(disabled=True)
agent = Agent(
name="test",
instructions="Respond with exactly: OK",
model="MiniMax-M2.5-highspeed",
)
result = await Runner.run(
agent,
"Confirm",
run_config=RunConfig(
model_provider=minimax_provider,
model_settings=ModelSettings(temperature=0.01, max_tokens=50),
),
)
assert result.final_output is not None
assert len(result.final_output) > 0
@pytest.mark.asyncio
@pytest.mark.timeout(120)
async def test_minimax_m25(minimax_provider: MiniMaxProvider) -> None:
"""MiniMax-M2.5 should return a response."""
set_tracing_disabled(disabled=True)
agent = Agent(
name="test",
instructions="Respond with exactly: OK",
model="MiniMax-M2.5",
)
result = await Runner.run(
agent,
"Confirm",
run_config=RunConfig(
model_provider=minimax_provider,
model_settings=ModelSettings(temperature=0.01, max_tokens=50),
),
)
assert result.final_output is not None
assert len(result.final_output) > 0