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
1 change: 1 addition & 0 deletions docs/modules/backend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The following table depicts supported providers. Each provider requires specific
| Google Gemini | ✅ | ✅ | GEMINI_CHAT_MODEL<br/>**GEMINI_API_KEY**<br/>GEMINI_API_HEADERS |
| MistralAI | ✅ | ✅ | MISTRALAI_CHAT_MODEL<br/>MISTRALAI_EMBEDDING_MODEL<br />**MISTRALAI_API_KEY**<br />MISTRALAI_API_BASE |
| Transformers | ✅ | ✅ | TRANSFORMERS_CHAT_MODEL<br/>HF_TOKEN|
| MiniMax | ✅ | ❌ | MINIMAX_CHAT_MODEL<br/>**MINIMAX_API_KEY**<br/>MINIMAX_API_BASE<br/>MINIMAX_API_HEADERS |

<Tip>
If you don't see your provider raise an issue [here](https://github.com/i-am-bee/beeai-framework/issues).
Expand Down
6 changes: 6 additions & 0 deletions python/beeai_framework/adapters/minimax/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from beeai_framework.adapters.minimax.backend.chat import MiniMaxChatModel

__all__ = ["MiniMaxChatModel"]
2 changes: 2 additions & 0 deletions python/beeai_framework/adapters/minimax/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0
70 changes: 70 additions & 0 deletions python/beeai_framework/adapters/minimax/backend/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

import os
from typing_extensions import Unpack

from beeai_framework.adapters.litellm import LiteLLMChatModel, utils
from beeai_framework.backend.chat import ChatModelKwargs
from beeai_framework.backend.constants import ProviderName
from beeai_framework.logger import Logger

logger = Logger(__name__)

MINIMAX_API_BASE = "https://api.minimax.io/v1"


class MiniMaxChatModel(LiteLLMChatModel):
"""
A chat model implementation for the MiniMax provider, leveraging LiteLLM.

MiniMax provides an OpenAI-compatible API. This adapter routes requests
through LiteLLM's OpenAI provider with the MiniMax base URL.

Available models include MiniMax-M2.7, MiniMax-M2.7-highspeed,
MiniMax-M2.5, and MiniMax-M2.5-highspeed.
"""

@property
def provider_id(self) -> ProviderName:
"""The provider ID for MiniMax."""
return "minimax"

def __init__(
self,
model_id: str | None = None,
*,
api_key: str | None = None,
base_url: str | None = None,
**kwargs: Unpack[ChatModelKwargs],
) -> None:
"""
Initializes the MinimaxChatModel.

Args:
model_id: The ID of the MiniMax model to use. If not provided,
it falls back to the MINIMAX_CHAT_MODEL environment variable,
and then defaults to 'MiniMax-M2.7'.
api_key: The MiniMax API key. Falls back to MINIMAX_API_KEY env var.
base_url: The MiniMax API base URL. Falls back to MINIMAX_API_BASE
env var, then defaults to 'https://api.minimax.io/v1'.
**kwargs: Additional settings to configure the provider.
"""
super().__init__(
model_id if model_id else os.getenv("MINIMAX_CHAT_MODEL", "MiniMax-M2.7"),
provider_id="openai",
**kwargs,
)

self._assert_setting_value("api_key", api_key, envs=["MINIMAX_API_KEY"])
self._assert_setting_value(
"base_url",
base_url,
envs=["MINIMAX_API_BASE"],
aliases=["api_base"],
allow_empty=True,
fallback=MINIMAX_API_BASE,
)
self._settings["extra_headers"] = utils.parse_extra_headers(
self._settings.get("extra_headers"), os.getenv("MINIMAX_API_HEADERS")
)
3 changes: 3 additions & 0 deletions python/beeai_framework/backend/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"transformers",
"deepseek",
"qwen",
"minimax",
]
ProviderHumanName = Literal[
"AgentStack",
Expand All @@ -44,6 +45,7 @@
"Transformers",
"Deepseek",
"Qwen",
"MiniMax",
]

ModelTypes = Literal["embedding", "chat"]
Expand Down Expand Up @@ -95,4 +97,5 @@ class ProviderModuleDef(BaseModel):
"Transformers": ProviderDef(name="Transformers", module="transformers", aliases=["Transformers", "transformers"]),
"Deepseek": ProviderDef(name="Deepseek", module="deepseek", aliases=["deepseek"]),
"Qwen": ProviderDef(name="Qwen", module="qwen", aliases=["qwen", "dashscope"]),
"MiniMax": ProviderDef(name="MiniMax", module="minimax", aliases=["minimax"]),
}
101 changes: 101 additions & 0 deletions python/examples/backend/providers/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

import asyncio

from dotenv import load_dotenv
from pydantic import BaseModel, Field

from beeai_framework.adapters.minimax import MiniMaxChatModel
from beeai_framework.backend import ChatModel, ChatModelNewTokenEvent, UserMessage
from beeai_framework.emitter import EventMeta
from beeai_framework.errors import AbortError
from beeai_framework.parsers.field import ParserField
from beeai_framework.parsers.line_prefix import LinePrefixParser, LinePrefixParserNode
from beeai_framework.utils import AbortSignal


async def minimax_from_name() -> None:
llm = ChatModel.from_name("minimax:MiniMax-M2.7")
user_message = UserMessage("what states are part of New England?")
response = await llm.run([user_message])
print(response.get_text_content())


async def minimax_sync() -> None:
llm = MiniMaxChatModel("MiniMax-M2.7")
user_message = UserMessage("what is the capital of Massachusetts?")
response = await llm.run([user_message])
print(response.get_text_content())


async def minimax_stream() -> None:
llm = MiniMaxChatModel("MiniMax-M2.7")
user_message = UserMessage("How many islands make up the country of Cape Verde?")
response = await llm.run([user_message], stream=True)
print(response.get_text_content())


async def minimax_stream_abort() -> None:
llm = MiniMaxChatModel("MiniMax-M2.7")
user_message = UserMessage("What is the smallest of the Cape Verde islands?")

try:
response = await llm.run([user_message], stream=True, signal=AbortSignal.timeout(0.5))

if response is not None:
print(response.get_text_content())
else:
print("No response returned.")
except AbortError as err:
print(f"Aborted: {err}")


async def minimax_structure() -> None:
class TestSchema(BaseModel):
answer: str = Field(description="your final answer")

llm = MiniMaxChatModel("MiniMax-M2.7")
user_message = UserMessage("How many islands make up the country of Cape Verde?")
response = await llm.run([user_message], response_format=TestSchema)
print(response.output_structured)


async def minimax_stream_parser() -> None:
llm = MiniMaxChatModel("MiniMax-M2.7")

parser = LinePrefixParser(
nodes={
"test": LinePrefixParserNode(
prefix="Prefix: ", field=ParserField.from_type(str), is_start=True, is_end=True
)
}
)

async def on_new_token(data: ChatModelNewTokenEvent, event: EventMeta) -> None:
await parser.add(chunk=data.value.get_text_content())

user_message = UserMessage("Produce 3 lines each starting with 'Prefix: ' followed by a sentence and a new line.")
await llm.run([user_message], stream=True).observe(lambda emitter: emitter.on("new_token", on_new_token))
result = await parser.end()
print(result)


async def main() -> None:
print("*" * 10, "minimax_from_name")
await minimax_from_name()
print("*" * 10, "minimax_sync")
await minimax_sync()
print("*" * 10, "minimax_stream")
await minimax_stream()
print("*" * 10, "minimax_stream_abort")
await minimax_stream_abort()
print("*" * 10, "minimax_structure")
await minimax_structure()
print("*" * 10, "minimax_stream_parser")
await minimax_stream_parser()


if __name__ == "__main__":
load_dotenv()
asyncio.run(main())
2 changes: 2 additions & 0 deletions python/tests/adapters/minimax/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0
111 changes: 111 additions & 0 deletions python/tests/adapters/minimax/test_minimax_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

import os
from unittest.mock import patch

import pytest

from beeai_framework.adapters.minimax.backend.chat import MINIMAX_API_BASE, MiniMaxChatModel
from beeai_framework.backend.chat import ChatModel
from beeai_framework.backend.constants import BackendProviders


class TestMiniMaxProviderRegistration:
"""Test that MiniMax is properly registered as a provider."""

def test_minimax_in_backend_providers(self) -> None:
assert "MiniMax" in BackendProviders
provider = BackendProviders["MiniMax"]
assert provider.name == "MiniMax"
assert provider.module == "minimax"
assert "minimax" in provider.aliases

def test_provider_def_has_correct_structure(self) -> None:
provider = BackendProviders["MiniMax"]
assert hasattr(provider, "name")
assert hasattr(provider, "module")
assert hasattr(provider, "aliases")


class TestMiniMaxChatModelInit:
"""Test MiniMaxChatModel initialization."""

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_default_model_id(self) -> None:
model = MiniMaxChatModel()
assert model.model_id == "MiniMax-M2.7"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_custom_model_id(self) -> None:
model = MiniMaxChatModel("MiniMax-M2.5")
assert model.model_id == "MiniMax-M2.5"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_highspeed_model_id(self) -> None:
model = MiniMaxChatModel("MiniMax-M2.7-highspeed")
assert model.model_id == "MiniMax-M2.7-highspeed"

@patch.dict(
os.environ,
{"MINIMAX_API_KEY": "test-key-123", "MINIMAX_CHAT_MODEL": "MiniMax-M2.5-highspeed"},
)
def test_model_from_env(self) -> None:
model = MiniMaxChatModel()
assert model.model_id == "MiniMax-M2.5-highspeed"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_provider_id(self) -> None:
model = MiniMaxChatModel()
assert model.provider_id == "minimax"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_default_base_url(self) -> None:
model = MiniMaxChatModel()
assert model._settings.get("base_url") == MINIMAX_API_BASE

@patch.dict(
os.environ,
{"MINIMAX_API_KEY": "test-key-123", "MINIMAX_API_BASE": "https://custom.minimax.io/v1"},
)
def test_custom_base_url_from_env(self) -> None:
model = MiniMaxChatModel()
assert model._settings.get("base_url") == "https://custom.minimax.io/v1"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_custom_base_url_param(self) -> None:
model = MiniMaxChatModel(base_url="https://proxy.example.com/v1")
assert model._settings.get("base_url") == "https://proxy.example.com/v1"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_api_key_stored(self) -> None:
model = MiniMaxChatModel()
assert model._settings.get("api_key") == "test-key-123"

def test_missing_api_key_raises(self) -> None:
with patch.dict(os.environ, {}, clear=True):
# Remove any existing MINIMAX_API_KEY
os.environ.pop("MINIMAX_API_KEY", None)
with pytest.raises(ValueError, match="api_key.*required"):
MiniMaxChatModel()

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_explicit_api_key(self) -> None:
model = MiniMaxChatModel(api_key="explicit-key")
assert model._settings.get("api_key") == "explicit-key"


class TestMiniMaxModelLoading:
"""Test that MiniMax models can be loaded via the factory method."""

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_load_from_name(self) -> None:
model = ChatModel.from_name("minimax:MiniMax-M2.7")
assert isinstance(model, MiniMaxChatModel)
assert model.model_id == "MiniMax-M2.7"

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_load_from_alias(self) -> None:
model = ChatModel.from_name("minimax:MiniMax-M2.5")
assert isinstance(model, MiniMaxChatModel)
assert model.model_id == "MiniMax-M2.5"
59 changes: 59 additions & 0 deletions python/tests/adapters/minimax/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

"""Integration tests for MiniMax chat model.

These tests require a valid MINIMAX_API_KEY environment variable.
Run with: pytest tests/adapters/minimax/test_minimax_integration.py -v
"""

import os

import pytest

from beeai_framework.adapters.minimax.backend.chat import MiniMaxChatModel
from beeai_framework.backend.message import UserMessage

pytestmark = pytest.mark.skipif(
not os.getenv("MINIMAX_API_KEY"),
reason="MINIMAX_API_KEY not set",
)


@pytest.fixture
def chat_model() -> MiniMaxChatModel:
return MiniMaxChatModel("MiniMax-M2.7")


@pytest.fixture
def highspeed_model() -> MiniMaxChatModel:
return MiniMaxChatModel("MiniMax-M2.7-highspeed")


class TestMiniMaxIntegration:
"""Integration tests that call the real MiniMax API."""

@pytest.mark.asyncio
async def test_simple_chat(self, chat_model: MiniMaxChatModel) -> None:
output = await chat_model.run(
[UserMessage("What is 2 + 2? Reply with just the number.")],
)
text = output.get_text_content()
assert "4" in text

@pytest.mark.asyncio
async def test_highspeed_model(self, highspeed_model: MiniMaxChatModel) -> None:
output = await highspeed_model.run(
[UserMessage("Say hello in one word.")],
)
text = output.get_text_content()
assert len(text) > 0

@pytest.mark.asyncio
async def test_streaming(self, chat_model: MiniMaxChatModel) -> None:
output = await chat_model.run(
[UserMessage("Count from 1 to 3.")],
stream=True,
)
text = output.get_text_content()
assert "1" in text
1 change: 1 addition & 0 deletions python/tests/examples/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"backend/providers/langchain_compatible.py",
"backend/providers/qwen.py" if os.getenv("DASHSCOPE_API_KEY") is None else None,
"backend/providers/deepseek.py" if os.getenv("DEEPSEEK_CHAT_MODEL") is None else None,
"backend/providers/minimax.py" if os.getenv("MINIMAX_API_KEY") is None else None,
"tools/mcp_agent.py" if os.getenv("SLACK_BOT_TOKEN") is None else None,
"tools/mcp_tool_creation.py" if os.getenv("SLACK_BOT_TOKEN") is None else None,
"tools/mcp_slack_agent.py" if os.getenv("SLACK_BOT_TOKEN") is None else None,
Expand Down
Loading
Loading