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
2 changes: 2 additions & 0 deletions api/oss/src/core/secrets/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class StandardProviderKind(str, Enum):
TOGETHERAI = "together_ai"
OPENROUTER = "openrouter"
GEMINI = "gemini"
MINIMAX = "minimax"


class CustomProviderKind(str, Enum):
Expand All @@ -43,3 +44,4 @@ class CustomProviderKind(str, Enum):
TOGETHERAI = "together_ai"
OPENROUTER = "openrouter"
GEMINI = "gemini"
MINIMAX = "minimax"
6 changes: 6 additions & 0 deletions sdk/agenta/sdk/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@
"perplexity/sonar-reasoning",
"perplexity/sonar-reasoning-pro",
],
"minimax": [
"minimax/MiniMax-M2.7",
"minimax/MiniMax-M2.7-highspeed",
"minimax/MiniMax-M2.5",
"minimax/MiniMax-M2.5-lightning",
],
"together_ai": [
"together_ai/deepseek-ai/DeepSeek-R1",
"together_ai/deepseek-ai/DeepSeek-V3",
Expand Down
186 changes: 186 additions & 0 deletions sdk/tests/pytest/integration/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Integration tests for MiniMax provider support.

These tests verify that MiniMax models work correctly through the full
provider resolution pipeline, including secret parsing, model lookup,
and SecretsManager normalization.

Note: These tests do not require a running MiniMax API or valid API key.
They verify the integration plumbing rather than actual API calls.
"""

import importlib.util
import os
import sys
import types

import pytest

# -------------------------------------------------------------------
# Load modules directly to avoid complex agenta SDK init chain.
# Mock litellm to avoid env-specific openai compatibility issues.
# -------------------------------------------------------------------
_SDK_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
_REPO_ROOT = os.path.abspath(os.path.join(_SDK_ROOT, ".."))
_API_ROOT = os.path.join(_REPO_ROOT, "api")

# Mock litellm before anything imports it
_mock_litellm = types.ModuleType("litellm")
_mock_cost_calculator = types.ModuleType("litellm.cost_calculator")
_mock_cost_calculator.cost_per_token = lambda **kw: None
_mock_litellm.cost_calculator = _mock_cost_calculator
sys.modules.setdefault("litellm", _mock_litellm)
sys.modules.setdefault("litellm.cost_calculator", _mock_cost_calculator)

# Register agenta SDK parent packages as real modules
for _pkg in ["agenta", "agenta.sdk", "agenta.sdk.utils", "agenta.sdk.utils.logging",
"agenta.sdk.contexts", "agenta.sdk.contexts.routing",
"agenta.sdk.contexts.running", "agenta.sdk.middlewares",
"agenta.sdk.middlewares.running", "agenta.sdk.middlewares.running.vault"]:
sys.modules.setdefault(_pkg, types.ModuleType(_pkg))

# Provide mock implementations for the modules secrets.py imports
_log_mod = sys.modules["agenta.sdk.utils.logging"]
if not hasattr(_log_mod, "get_module_logger"):
_log_mod.get_module_logger = lambda name: types.SimpleNamespace(
warning=lambda *a, **kw: None, info=lambda *a, **kw: None,
)
_routing_mod = sys.modules["agenta.sdk.contexts.routing"]
if not hasattr(_routing_mod, "RoutingContext"):
_routing_mod.RoutingContext = types.SimpleNamespace(get=lambda: None)
_running_mod = sys.modules["agenta.sdk.contexts.running"]
if not hasattr(_running_mod, "RunningContext"):
_running_mod.RunningContext = types.SimpleNamespace(get=lambda: None)
_vault_mod = sys.modules["agenta.sdk.middlewares.running.vault"]
if not hasattr(_vault_mod, "get_secrets"):
_vault_mod.get_secrets = lambda *a, **kw: ([], [], [])


def _load_module(name: str, filepath: str):
spec = importlib.util.spec_from_file_location(name, filepath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


_assets = _load_module(
"assets", os.path.join(_SDK_ROOT, "agenta", "sdk", "assets.py")
)
# Register so secrets.py can find it
sys.modules["agenta.sdk.assets"] = _assets

_secrets = _load_module(
"secrets_manager", os.path.join(_SDK_ROOT, "agenta", "sdk", "managers", "secrets.py")
)
SecretsManager = _secrets.SecretsManager

_enums = _load_module(
"enums", os.path.join(_API_ROOT, "oss", "src", "core", "secrets", "enums.py")
)


class TestMiniMaxSecretParsing:
"""Tests for MiniMax secret parsing through SecretsManager."""

def test_standard_secret_roundtrip(self):
raw = [
{
"kind": "provider_key",
"data": {
"kind": "minimax",
"provider": {"key": "test-minimax-key-12345"},
},
}
]
parsed = SecretsManager._parse_secrets(raw)
assert len(parsed) == 1
assert parsed[0]["kind"] == "provider_key"
assert parsed[0]["data"]["kind"] == "minimax"
assert parsed[0]["data"]["provider"]["key"] == "test-minimax-key-12345"

def test_custom_provider_secret_roundtrip(self):
raw = [
{
"kind": "custom_provider",
"data": {
"kind": "minimax",
"provider_slug": "my-minimax",
"provider": {
"url": "https://api.minimax.io/v1",
"extras": {"api_key": "test-key"},
},
"model_keys": [
"my-minimax/minimax/MiniMax-M2.7",
"my-minimax/minimax/MiniMax-M2.5",
],
},
}
]
parsed = SecretsManager._parse_secrets(raw)
assert len(parsed) == 1
assert parsed[0]["kind"] == "custom_provider"
provider = parsed[0]["data"]["provider"]
assert provider["kind"] == "minimax"
assert provider["extras"]["api_base"] == "https://api.minimax.io/v1"
assert provider["extras"]["api_key"] == "test-key"

def test_minimax_among_multiple_providers(self):
"""MiniMax should be correctly identified among multiple provider secrets."""
raw = [
{
"kind": "provider_key",
"data": {"kind": "openai", "provider": {"key": "openai-key"}},
},
{
"kind": "provider_key",
"data": {"kind": "minimax", "provider": {"key": "minimax-key"}},
},
{
"kind": "provider_key",
"data": {"kind": "anthropic", "provider": {"key": "anthro-key"}},
},
]
parsed = SecretsManager._parse_secrets(raw)
assert len(parsed) == 3
minimax_secrets = [s for s in parsed if s["data"]["kind"] == "minimax"]
assert len(minimax_secrets) == 1
assert minimax_secrets[0]["data"]["provider"]["key"] == "minimax-key"


class TestMiniMaxProviderNormalization:
"""Tests for MiniMax provider kind normalization."""

def test_lowercase(self):
assert SecretsManager._normalize_provider_kind("minimax") == "minimax"

def test_mixed_case(self):
assert SecretsManager._normalize_provider_kind("MiniMax") == "minimax"

def test_uppercase(self):
assert SecretsManager._normalize_provider_kind("MINIMAX") == "minimax"

def test_with_space(self):
assert SecretsManager._normalize_provider_kind("Mini Max") == "minimax"

def test_with_hyphen(self):
assert SecretsManager._normalize_provider_kind("Mini-Max") == "minimax"


class TestMiniMaxEnumConsistency:
"""Tests for MiniMax enum consistency across Standard and Custom provider kinds."""

def test_enum_values_match(self):
assert (
_enums.StandardProviderKind.MINIMAX.value
== _enums.CustomProviderKind.MINIMAX.value
== "minimax"
)

def test_model_naming_consistency(self):
for model in _assets.supported_llm_models["minimax"]:
assert model.startswith("minimax/"), (
f"Model {model} must use 'minimax/' prefix for LiteLLM routing"
)
model_name = model.split("/", 1)[1]
assert model_name.startswith("MiniMax-"), (
f"Model name {model_name} should start with 'MiniMax-'"
)
132 changes: 132 additions & 0 deletions sdk/tests/pytest/unit/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Unit tests for MiniMax provider integration.

These tests verify that MiniMax is correctly registered as a first-class
LLM provider across the backend enums and SDK model registry.
"""

import importlib.util
import os
import sys
import types

import pytest

# -------------------------------------------------------------------
# Load modules directly to avoid complex agenta SDK init chain.
# Mock litellm to avoid broken openai._models dependency in CI.
# -------------------------------------------------------------------
_SDK_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
_REPO_ROOT = os.path.abspath(os.path.join(_SDK_ROOT, ".."))
_API_ROOT = os.path.join(_REPO_ROOT, "api")

# Mock litellm.cost_calculator so assets.py can be loaded without litellm
_mock_litellm = types.ModuleType("litellm")
_mock_cost_calculator = types.ModuleType("litellm.cost_calculator")
_mock_cost_calculator.cost_per_token = lambda **kw: None
_mock_litellm.cost_calculator = _mock_cost_calculator
sys.modules.setdefault("litellm", _mock_litellm)
sys.modules.setdefault("litellm.cost_calculator", _mock_cost_calculator)


def _load_module(name: str, filepath: str):
spec = importlib.util.spec_from_file_location(name, filepath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


_assets = _load_module(
"assets", os.path.join(_SDK_ROOT, "agenta", "sdk", "assets.py")
)
supported_llm_models = _assets.supported_llm_models
model_to_provider_mapping = _assets.model_to_provider_mapping
providers_list = _assets.providers_list

_enums = _load_module(
"enums", os.path.join(_API_ROOT, "oss", "src", "core", "secrets", "enums.py")
)
StandardProviderKind = _enums.StandardProviderKind
CustomProviderKind = _enums.CustomProviderKind


class TestMiniMaxModels:
"""Tests for MiniMax model registration in supported_llm_models."""

def test_minimax_provider_exists(self):
assert "minimax" in supported_llm_models

def test_minimax_models_not_empty(self):
assert len(supported_llm_models["minimax"]) > 0

def test_minimax_m27_model_registered(self):
assert "minimax/MiniMax-M2.7" in supported_llm_models["minimax"]

def test_minimax_m27_highspeed_model_registered(self):
assert "minimax/MiniMax-M2.7-highspeed" in supported_llm_models["minimax"]

def test_minimax_m25_model_registered(self):
assert "minimax/MiniMax-M2.5" in supported_llm_models["minimax"]

def test_minimax_m25_lightning_model_registered(self):
assert "minimax/MiniMax-M2.5-lightning" in supported_llm_models["minimax"]

def test_minimax_models_use_provider_prefix(self):
for model in supported_llm_models["minimax"]:
assert model.startswith("minimax/"), (
f"Model {model} should use 'minimax/' prefix"
)


class TestMiniMaxModelProviderMapping:
"""Tests for MiniMax model-to-provider mapping."""

def test_minimax_m27_maps_to_minimax_provider(self):
assert model_to_provider_mapping.get("minimax/MiniMax-M2.7") == "minimax"

def test_minimax_m27_highspeed_maps_to_minimax_provider(self):
assert (
model_to_provider_mapping.get("minimax/MiniMax-M2.7-highspeed")
== "minimax"
)

def test_minimax_m25_maps_to_minimax_provider(self):
assert model_to_provider_mapping.get("minimax/MiniMax-M2.5") == "minimax"

def test_minimax_m25_lightning_maps_to_minimax_provider(self):
assert (
model_to_provider_mapping.get("minimax/MiniMax-M2.5-lightning") == "minimax"
)

def test_all_minimax_models_mapped(self):
for model in supported_llm_models["minimax"]:
assert model in model_to_provider_mapping, (
f"Model {model} should be in model_to_provider_mapping"
)
assert model_to_provider_mapping[model] == "minimax"


class TestMiniMaxProvidersList:
"""Tests for MiniMax in providers_list."""

def test_minimax_in_providers_list(self):
assert "minimax" in providers_list


class TestMiniMaxProviderEnums:
"""Tests for MiniMax in provider kind enums."""

def test_standard_provider_kind_has_minimax(self):
assert hasattr(StandardProviderKind, "MINIMAX")
assert StandardProviderKind.MINIMAX.value == "minimax"

def test_custom_provider_kind_has_minimax(self):
assert hasattr(CustomProviderKind, "MINIMAX")
assert CustomProviderKind.MINIMAX.value == "minimax"

def test_minimax_in_standard_provider_values(self):
values = {p.value for p in StandardProviderKind}
assert "minimax" in values

def test_minimax_in_custom_provider_values(self):
values = {p.value for p in CustomProviderKind}
assert "minimax" in values
2 changes: 2 additions & 0 deletions web/oss/src/lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export enum SecretDTOProvider {
TOGETHERAI = "together_ai",
OPENROUTER = "openrouter",
GEMINI = "gemini",
MINIMAX = "minimax",
}

export const PROVIDER_LABELS: Record<string, string> = {
Expand All @@ -318,6 +319,7 @@ export const PROVIDER_LABELS: Record<string, string> = {
together_ai: "Together AI",
openrouter: "OpenRouter",
gemini: "Google Gemini",
minimax: "MiniMax",
vertex_ai: "Google Vertex AI",
bedrock: "AWS Bedrock",
// sagemaker: "AWS SageMaker",
Expand Down
2 changes: 2 additions & 0 deletions web/oss/src/lib/helpers/llmProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const transformSecret = (secrets: CustomSecretDTO[] | StandardSecretDTO[]
together_ai: "TOGETHERAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
gemini: "GEMINI_API_KEY",
minimax: "MINIMAX_API_KEY",
}

acc.push({
Expand Down Expand Up @@ -96,6 +97,7 @@ export const llmAvailableProviders: LlmProvider[] = [
{title: "OpenRouter", key: "", name: "OPENROUTER_API_KEY"},
{title: "Groq", key: "", name: "GROQ_API_KEY"},
{title: "Google Gemini", key: "", name: "GEMINI_API_KEY"},
{title: "MiniMax", key: "", name: "MINIMAX_API_KEY"},
]

export const transformCustomProviderPayloadData = (values: LlmProvider) => {
Expand Down
Loading