Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,29 @@ 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.minimax.io (global) or https://platform.minimaxi.com (CN).
MINIMAX_API_KEY=

# MiniMax region — selects the endpoint set for both the OpenAI-compatible and
# Anthropic-compatible routes. Use "global" (default) or "cn".
# global: https://api.minimax.io/v1 https://api.minimax.io/anthropic
# cn: https://api.minimaxi.com/v1 https://api.minimaxi.com/anthropic
MINIMAX_REGION=global
# Optional explicit endpoint overrides (otherwise derived from MINIMAX_REGION):
# MINIMAX_API_BASE=
# MINIMAX_ANTHROPIC_BASE=


# ── Model selection ───────────────────────────

# Override the default model for all agents (set automatically by onboarding).
# OpenAI example: DEFAULT_MODEL=gpt-5.4
# Anthropic example: DEFAULT_MODEL=litellm/claude-sonnet-4-6
# Google example: DEFAULT_MODEL=litellm/gemini/gemini-3-flash
# MiniMax (OpenAI-compatible): DEFAULT_MODEL=minimax/MiniMax-M3
# MiniMax (OpenAI-compatible): DEFAULT_MODEL=minimax/MiniMax-M2.7
# MiniMax (Anthropic-compatible): DEFAULT_MODEL=anthropic/MiniMax-M3
DEFAULT_MODEL=


Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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-M3, MiniMax-M2.7); set `MINIMAX_REGION` to `global` or `cn`

**Optional superpowers:**

Expand Down
63 changes: 60 additions & 3 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
import os


# MiniMax exposes matching OpenAI-compatible and Anthropic-compatible endpoints
# on both a global and a mainland-China region. Selecting a region swaps the
# whole endpoint set so the same model strings work from either location.
MINIMAX_ENDPOINTS = {
"global": {
"openai_base_url": "https://api.minimax.io/v1",
"anthropic_base_url": "https://api.minimax.io/anthropic",
},
"cn": {
"openai_base_url": "https://api.minimaxi.com/v1",
"anthropic_base_url": "https://api.minimaxi.com/anthropic",
},
}


def get_default_model(fallback: str = "gpt-5.4"):
"""Return the configured default model for standard agents."""
model = os.getenv("DEFAULT_MODEL", fallback)
Expand All @@ -13,22 +28,64 @@ def is_openai_provider() -> bool:

OpenAI model IDs never contain a slash (e.g. 'gpt-5.4', 'o3').
Any 'provider/model' string (e.g. 'anthropic/claude-sonnet-4-6',
'litellm/gemini/gemini-3-flash') is treated as a LiteLLM-routed model.
'litellm/gemini/gemini-3-flash', 'minimax/MiniMax-M3') is treated as a
LiteLLM-routed model.
"""
return "/" not in os.getenv("DEFAULT_MODEL", "")


def is_minimax_model(model: str) -> bool:
"""Return True when ``model`` targets a MiniMax model on either routing style.

Covers the OpenAI-compatible form ('minimax/MiniMax-M3') and the
Anthropic-compatible form ('anthropic/MiniMax-M3'), regardless of case.
"""
return "minimax" in model.lower()


def minimax_region() -> str:
"""Return the selected MiniMax region ('global' or 'cn')."""
region = os.getenv("MINIMAX_REGION", "global").strip().lower()
return "cn" if region in {"cn", "cn_zh", "china"} else "global"


def minimax_base_url(model: str) -> str:
"""Return the MiniMax base URL for ``model`` in the configured region.

Uses the Anthropic-compatible endpoint when the model is routed through the
'anthropic/' provider, otherwise the OpenAI-compatible endpoint. Explicit
``MINIMAX_ANTHROPIC_BASE`` / ``MINIMAX_API_BASE`` overrides win when set.
"""
endpoints = MINIMAX_ENDPOINTS[minimax_region()]
if model.startswith("anthropic/"):
return os.getenv("MINIMAX_ANTHROPIC_BASE") or endpoints["anthropic_base_url"]
return os.getenv("MINIMAX_API_BASE") or endpoints["openai_base_url"]


def _minimax_kwargs(model: str) -> dict:
"""Build LitellmModel keyword arguments for a MiniMax model."""
kwargs = {"api_base": minimax_base_url(model)}
api_key = os.getenv("MINIMAX_API_KEY")
if api_key:
kwargs["api_key"] = api_key
return kwargs


def _resolve(model: str):
"""Route 'provider/model' strings through LitellmModel.

Handles both explicit 'litellm/<model>' and bare 'provider/model' forms.
OpenAI model IDs contain no slash, so they pass through unchanged.
OpenAI model IDs contain no slash, so they pass through unchanged. MiniMax
models additionally receive the region-selected base URL so both the
OpenAI-compatible and Anthropic-compatible endpoints route correctly.
"""
if "/" not in model:
return model
bare = model[len("litellm/"):] if model.startswith("litellm/") else model
try:
from agency_swarm import LitellmModel # noqa: PLC0415
return LitellmModel(model=bare)
except ImportError:
return model
if is_minimax_model(bare):
return LitellmModel(model=bare, **_minimax_kwargs(bare))
return LitellmModel(model=bare)
158 changes: 158 additions & 0 deletions tests/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Unit tests for MiniMax provider configuration in config.py.

These tests avoid any network calls and do not require ``agency_swarm`` to be
installed: ``config._resolve`` falls back to returning the raw model string when
the dependency is absent, so the region/endpoint selection logic is exercised
directly through the public helpers.
"""

from __future__ import annotations

import importlib
import os
import sys
from pathlib import Path
from unittest import mock

import pytest

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

import config # noqa: E402


@pytest.fixture(autouse=True)
def _reload_config():
"""Reload config with a clean MiniMax environment for each test."""
for key in ("DEFAULT_MODEL", "MINIMAX_REGION", "MINIMAX_API_BASE",
"MINIMAX_ANTHROPIC_BASE", "MINIMAX_API_KEY"):
os.environ.pop(key, None)
importlib.reload(config)
yield
importlib.reload(config)


class TestMiniMaxDetection:
def test_openai_compatible_form_detected(self):
assert config.is_minimax_model("minimax/MiniMax-M3") is True

def test_anthropic_compatible_form_detected(self):
assert config.is_minimax_model("anthropic/MiniMax-M2.7") is True

def test_case_insensitive(self):
assert config.is_minimax_model("MINIMAX/MINIMAX-M3") is True

def test_other_providers_not_detected(self):
assert config.is_minimax_model("anthropic/claude-sonnet-4-6") is False
assert config.is_minimax_model("litellm/gemini/gemini-3-flash") is False


class TestMiniMaxRegion:
def test_default_region_is_global(self):
assert config.minimax_region() == "global"

def test_cn_region_selected(self):
with mock.patch.dict(os.environ, {"MINIMAX_REGION": "cn"}):
assert config.minimax_region() == "cn"

def test_cn_zh_alias(self):
with mock.patch.dict(os.environ, {"MINIMAX_REGION": "cn_zh"}):
assert config.minimax_region() == "cn"


class TestMiniMaxBaseUrl:
def test_global_openai_endpoint(self):
assert config.minimax_base_url("minimax/MiniMax-M3") == "https://api.minimax.io/v1"

def test_global_anthropic_endpoint(self):
assert config.minimax_base_url("anthropic/MiniMax-M3") == "https://api.minimax.io/anthropic"

def test_cn_openai_endpoint(self):
with mock.patch.dict(os.environ, {"MINIMAX_REGION": "cn"}):
assert config.minimax_base_url("minimax/MiniMax-M2.7") == "https://api.minimaxi.com/v1"

def test_cn_anthropic_endpoint(self):
with mock.patch.dict(os.environ, {"MINIMAX_REGION": "cn"}):
assert config.minimax_base_url("anthropic/MiniMax-M3") == "https://api.minimaxi.com/anthropic"

def test_explicit_openai_override_wins(self):
with mock.patch.dict(os.environ, {"MINIMAX_API_BASE": "https://proxy.example/v1"}):
assert config.minimax_base_url("minimax/MiniMax-M3") == "https://proxy.example/v1"

def test_explicit_anthropic_override_wins(self):
with mock.patch.dict(os.environ, {"MINIMAX_ANTHROPIC_BASE": "https://proxy.example/anthropic"}):
assert config.minimax_base_url("anthropic/MiniMax-M3") == "https://proxy.example/anthropic"


class TestMiniMaxKwargs:
def test_api_base_always_present(self):
kwargs = config._minimax_kwargs("minimax/MiniMax-M3")
assert kwargs["api_base"] == "https://api.minimax.io/v1"
assert "api_key" not in kwargs

def test_api_key_included_when_set(self):
with mock.patch.dict(os.environ, {"MINIMAX_API_KEY": "unit-test-placeholder"}):
kwargs = config._minimax_kwargs("anthropic/MiniMax-M3")
assert kwargs["api_base"] == "https://api.minimax.io/anthropic"
assert kwargs["api_key"] == "unit-test-placeholder"


class TestResolveAndDefaultModel:
def test_minimax_openai_route_resolves(self):
result = config._resolve("minimax/MiniMax-M3")
try:
from agency_swarm import LitellmModel
assert isinstance(result, LitellmModel)
except ImportError:
assert result == "minimax/MiniMax-M3"

def test_minimax_anthropic_route_resolves(self):
result = config._resolve("anthropic/MiniMax-M2.7")
try:
from agency_swarm import LitellmModel
assert isinstance(result, LitellmModel)
except ImportError:
assert result == "anthropic/MiniMax-M2.7"

def test_minimax_model_is_not_openai_provider(self):
with mock.patch.dict(os.environ, {"DEFAULT_MODEL": "minimax/MiniMax-M3"}):
importlib.reload(config)
assert config.is_openai_provider() is False

def test_get_default_model_with_minimax(self):
with mock.patch.dict(os.environ, {"DEFAULT_MODEL": "minimax/MiniMax-M2.7"}):
importlib.reload(config)
result = config.get_default_model()
try:
from agency_swarm import LitellmModel
assert isinstance(result, LitellmModel)
except ImportError:
assert result == "minimax/MiniMax-M2.7"


class TestEnvExample:
def _content(self):
return (ROOT / ".env.example").read_text(encoding="utf-8")

def test_api_key_documented(self):
assert "MINIMAX_API_KEY" in self._content()

def test_region_documented(self):
assert "MINIMAX_REGION" in self._content()

def test_both_models_documented(self):
content = self._content()
assert "MiniMax-M3" in content
assert "MiniMax-M2.7" in content

def test_both_routes_documented(self):
content = self._content()
assert "minimax/MiniMax-M3" in content
assert "anthropic/MiniMax-M3" in content

def test_both_regions_documented(self):
content = self._content()
assert "api.minimax.io" in content
assert "api.minimaxi.com" in content