From 68554f1dd3fde1144ca5118e948ff23d24a9bbb8 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Sun, 22 Mar 2026 15:50:04 +0800 Subject: [PATCH 1/2] feat: add MiniMax as first-class LLM provider Add MiniMaxClient extending OpenAIClient for MiniMax's OpenAI-compatible API. Supports MiniMax-M2.7, MiniMax-M2.5, and MiniMax-M2.5-highspeed models via MINIMAX_API_KEY environment variable. - New minimax_client.py with MiniMaxClient class - Register MiniMaxClient in model_client __init__.py - Add 12 unit tests covering init, sync/async clients, Generator integration, serialization, and input conversion --- .../components/model_client/__init__.py | 5 + .../components/model_client/minimax_client.py | 77 ++++++ adalflow/tests/test_minimax_client.py | 257 ++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 adalflow/adalflow/components/model_client/minimax_client.py create mode 100644 adalflow/tests/test_minimax_client.py diff --git a/adalflow/adalflow/components/model_client/__init__.py b/adalflow/adalflow/components/model_client/__init__.py index b331155ea..fc569433a 100644 --- a/adalflow/adalflow/components/model_client/__init__.py +++ b/adalflow/adalflow/components/model_client/__init__.py @@ -85,6 +85,10 @@ "adalflow.components.model_client.azureai_client.AzureAIClient", OptionalPackages.AZURE, ) +MiniMaxClient = LazyImport( + "adalflow.components.model_client.minimax_client.MiniMaxClient", + OptionalPackages.OPENAI, +) get_first_message_content = LazyImport( "adalflow.components.model_client.openai_client.get_first_message_content", OptionalPackages.OPENAI, @@ -124,6 +128,7 @@ "FireworksClient", "SambaNovaClient", "AzureAIClient", + "MiniMaxClient", # Utils functions "process_images_for_response_api", "format_content_for_response_api", diff --git a/adalflow/adalflow/components/model_client/minimax_client.py b/adalflow/adalflow/components/model_client/minimax_client.py new file mode 100644 index 000000000..bda0fd298 --- /dev/null +++ b/adalflow/adalflow/components/model_client/minimax_client.py @@ -0,0 +1,77 @@ +from typing import Optional, Any, Callable, Literal +from openai.types import Completion +from adalflow.components.model_client.openai_client import OpenAIClient + +BASE_URL = "https://api.minimax.io/v1" + +__all__ = ["MiniMaxClient"] + + +class MiniMaxClient(OpenAIClient): + + __doc__ = r"""A component wrapper for MiniMax's OpenAI-compatible API. + +MiniMax provides large language models accessible through an OpenAI-compatible API. +This client extends :class:`OpenAIClient` and customizes: + - The base URL to ``https://api.minimax.io/v1`` + - The API key environment variable to ``MINIMAX_API_KEY`` + +Available models include ``MiniMax-M2.7``, ``MiniMax-M2.5``, and ``MiniMax-M2.5-highspeed`` +(204K context window). + +References: + - To obtain your API key, sign up at: https://www.minimaxi.com/ + - API documentation: https://www.minimaxi.com/document/introduction + +**Example usage with the AdalFlow Generator:** + +.. code-block:: python + + from adalflow.core import Generator + from adalflow.components.model_client.minimax_client import MiniMaxClient + from adalflow.utils import setup_env + + setup_env() + + generator = Generator( + model_client=MiniMaxClient(), + model_kwargs={ + "model": "MiniMax-M2.7", + "temperature": 0.7, + "stream": False, + } + ) + + output = generator(prompt_kwargs={"input_str": "Hello! Tell me about yourself."}) + """ + + def __init__( + self, + api_key: Optional[str] = None, + non_streaming_chat_completion_parser: Callable[[Completion], Any] = None, + streaming_chat_completion_parser: Callable[[Completion], Any] = None, + input_type: Literal["text", "messages"] = "text", + base_url: str = BASE_URL, + env_api_key_name: str = "MINIMAX_API_KEY", + ): + """ + Initialize a MiniMaxClient instance. + + :param api_key: (Optional) MiniMax API key. If not provided, the client + attempts to read from the environment variable ``MINIMAX_API_KEY``. + :param non_streaming_chat_completion_parser: (Optional) A custom function to parse non-streaming responses. + :param streaming_chat_completion_parser: (Optional) A custom function to parse streaming responses. + :param input_type: Specifies the input format, either ``"text"`` or ``"messages"``. + Defaults to ``"text"``. + :param base_url: MiniMax API endpoint. Defaults to ``"https://api.minimax.io/v1"``. + :param env_api_key_name: The name of the environment variable holding the API key. + Defaults to ``MINIMAX_API_KEY``. + """ + super().__init__( + api_key=api_key, + non_streaming_chat_completion_parser=non_streaming_chat_completion_parser, + streaming_chat_completion_parser=streaming_chat_completion_parser, + input_type=input_type, + base_url=base_url, + env_api_key_name=env_api_key_name, + ) diff --git a/adalflow/tests/test_minimax_client.py b/adalflow/tests/test_minimax_client.py new file mode 100644 index 000000000..fada010d2 --- /dev/null +++ b/adalflow/tests/test_minimax_client.py @@ -0,0 +1,257 @@ +import unittest +from unittest.mock import patch, Mock, AsyncMock + +from openai.types.responses import Response + +from adalflow.components.model_client.minimax_client import MiniMaxClient +from adalflow.core import Generator +from adalflow.core.types import ModelType, GeneratorOutput +from adalflow.utils import get_logger + + +def getenv_side_effect(key): + """Mock environment variables for testing.""" + env_vars = {"MINIMAX_API_KEY": "fake_minimax_api_key"} + return env_vars.get(key, None) + + +class TestMiniMaxClient(unittest.IsolatedAsyncioTestCase): + """Test MiniMax client with proper mocking.""" + + def setUp(self): + """Set up test fixtures.""" + self.log = get_logger(level="DEBUG") + self.prompt_kwargs = {"input_str": "What is the meaning of life?"} + + # Mock response for testing using OpenAI Response API + self.mock_response = Mock(spec=Response) + self.mock_response.output_text = ( + "The meaning of life is to find purpose and meaning in our existence." + ) + + # Create a mock usage object + mock_usage = Mock() + mock_usage.input_tokens = 25 + mock_usage.output_tokens = 15 + mock_usage.total_tokens = 40 + self.mock_response.usage = mock_usage + + self.api_kwargs = { + "input": "What is the meaning of life?", + "model": "MiniMax-M2.7", + } + + def test_minimax_client_init(self): + """Test MiniMax client initialization.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key") + + # Test basic properties + self.assertEqual(client.base_url, "https://api.minimax.io/v1") + self.assertEqual(client._env_api_key_name, "MINIMAX_API_KEY") + self.assertEqual(client._input_type, "text") + + def test_minimax_client_init_custom_base_url(self): + """Test MiniMax client initialization with custom base URL.""" + with patch("os.getenv", side_effect=getenv_side_effect): + custom_url = "https://custom.minimax.io/v1" + client = MiniMaxClient(api_key="fake_api_key", base_url=custom_url) + self.assertEqual(client.base_url, custom_url) + + def test_minimax_client_init_messages_input_type(self): + """Test MiniMax client initialization with messages input type.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key", input_type="messages") + self.assertEqual(client._input_type, "messages") + + @patch("os.getenv") + def test_minimax_init_sync_client(self, mock_os_getenv): + """Test sync client initialization.""" + mock_os_getenv.return_value = "fake_api_key" + client = MiniMaxClient(api_key="fake_api_key") + + # Test that sync client is properly initialized + self.assertIsNotNone(client.sync_client) + self.assertEqual(client.sync_client.api_key, "fake_api_key") + self.assertEqual( + client.sync_client.base_url, "https://api.minimax.io/v1/" + ) + + @patch("os.getenv") + def test_minimax_init_async_client(self, mock_os_getenv): + """Test async client initialization.""" + mock_os_getenv.return_value = "fake_api_key" + client = MiniMaxClient(api_key="fake_api_key") + + # Initialize async client + client.async_client = client.init_async_client() + + # Test that async client is properly initialized + self.assertIsNotNone(client.async_client) + self.assertEqual(client.async_client.api_key, "fake_api_key") + self.assertEqual( + client.async_client.base_url, "https://api.minimax.io/v1/" + ) + + @patch("adalflow.components.model_client.openai_client.AsyncOpenAI") + async def test_minimax_acall_llm(self, MockAsyncOpenAI): + """Test async LLM call.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key") + + mock_async_client = AsyncMock() + MockAsyncOpenAI.return_value = mock_async_client + mock_async_client.responses.create = AsyncMock( + return_value=self.mock_response + ) + + # Call the acall method + result = await client.acall( + api_kwargs=self.api_kwargs, model_type=ModelType.LLM + ) + + # Assertions + MockAsyncOpenAI.assert_called_once() + mock_async_client.responses.create.assert_awaited_once_with( + **self.api_kwargs + ) + self.assertEqual(result, self.mock_response) + + @patch( + "adalflow.components.model_client.openai_client.OpenAIClient.init_sync_client" + ) + @patch("adalflow.components.model_client.openai_client.OpenAI") + def test_minimax_call(self, MockOpenAI, mock_init_sync_client): + """Test sync LLM call.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key") + + mock_sync_client = Mock() + MockOpenAI.return_value = mock_sync_client + mock_init_sync_client.return_value = mock_sync_client + mock_sync_client.responses.create = Mock(return_value=self.mock_response) + + # Set the sync client + client.sync_client = mock_sync_client + + # Call the call method + result = client.call(api_kwargs=self.api_kwargs, model_type=ModelType.LLM) + + # Assertions + mock_sync_client.responses.create.assert_called_once_with(**self.api_kwargs) + self.assertEqual(result, self.mock_response) + + # Test parse_chat_completion + output = client.parse_chat_completion(completion=self.mock_response) + self.assertTrue(isinstance(output, GeneratorOutput)) + self.assertEqual( + output.raw_response, + "The meaning of life is to find purpose and meaning in our existence.", + ) + self.assertEqual(output.usage.output_tokens, 15) + self.assertEqual(output.usage.input_tokens, 25) + self.assertEqual(output.usage.total_tokens, 40) + + @patch( + "adalflow.components.model_client.openai_client.OpenAIClient.init_sync_client" + ) + @patch("adalflow.components.model_client.openai_client.OpenAI") + def test_minimax_generator_integration(self, MockOpenAI, mock_init_sync_client): + """Test MiniMax client integration with Generator.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key") + + mock_sync_client = Mock() + MockOpenAI.return_value = mock_sync_client + mock_init_sync_client.return_value = mock_sync_client + mock_sync_client.responses.create = Mock(return_value=self.mock_response) + + # Set the sync client + client.sync_client = mock_sync_client + + # Create generator with mocked client + gen = Generator( + model_client=client, + model_kwargs={ + "model": "MiniMax-M2.7", + "temperature": 0.7, + "max_tokens": 1000, + }, + ) + + # Test generator call + response = gen(prompt_kwargs=self.prompt_kwargs) + + # Verify response + self.assertIsNotNone(response) + self.log.debug(f"Response: {response}") + + # Verify that the mock was called + mock_sync_client.responses.create.assert_called() + + def test_minimax_convert_inputs_to_api_kwargs(self): + """Test input conversion to API kwargs.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key") + + # Test text input conversion + api_kwargs = client.convert_inputs_to_api_kwargs( + input="Hello, world!", + model_kwargs={ + "model": "MiniMax-M2.7", + "temperature": 0.7, + }, + model_type=ModelType.LLM, + ) + + # Verify the structure for Response API + self.assertIn("input", api_kwargs) + self.assertIn("model", api_kwargs) + self.assertEqual(api_kwargs["model"], "MiniMax-M2.7") + self.assertEqual(api_kwargs["temperature"], 0.7) + + # Verify input content + self.assertEqual(api_kwargs["input"], "Hello, world!") + + def test_minimax_convert_inputs_m25_highspeed(self): + """Test input conversion with MiniMax-M2.5-highspeed model.""" + with patch("os.getenv", side_effect=getenv_side_effect): + client = MiniMaxClient(api_key="fake_api_key") + + api_kwargs = client.convert_inputs_to_api_kwargs( + input="Summarize this text.", + model_kwargs={ + "model": "MiniMax-M2.5-highspeed", + "temperature": 0.5, + }, + model_type=ModelType.LLM, + ) + + self.assertEqual(api_kwargs["model"], "MiniMax-M2.5-highspeed") + self.assertEqual(api_kwargs["temperature"], 0.5) + + def test_minimax_from_dict_to_dict(self): + """Test serialization and deserialization.""" + with patch("os.getenv", side_effect=getenv_side_effect): + test_api_key = "fake_api_key" + client = MiniMaxClient(api_key=test_api_key) + + # Test to_dict + client_dict = client.to_dict() + self.assertIn("data", client_dict) + self.assertIn("_api_key", client_dict["data"]) + self.assertEqual(client_dict["data"]["_api_key"], test_api_key) + + # Test from_dict + new_client = MiniMaxClient.from_dict(client_dict) + self.assertEqual(new_client.to_dict(), client_dict) + + def test_minimax_inherits_openai_client(self): + """Test that MiniMaxClient properly inherits from OpenAIClient.""" + from adalflow.components.model_client.openai_client import OpenAIClient + + self.assertTrue(issubclass(MiniMaxClient, OpenAIClient)) + + +if __name__ == "__main__": + unittest.main() From f04bbadc1645771fdf3d6d2675ddbf049deebc10 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sun, 7 Jun 2026 14:05:04 +0800 Subject: [PATCH 2/2] feat: set MiniMax-M3 as default model - Default model in docstring example: MiniMax-M2.7 -> MiniMax-M3 - Available models documented: MiniMax-M3 (default; 512K context, up to 128K output, image input), MiniMax-M2.7, MiniMax-M2.7-highspeed - Drop M2.5 / M2.5-highspeed mentions - Tests: switch primary model assertions to MiniMax-M3; rename highspeed test to MiniMax-M2.7-highspeed --- .../components/model_client/minimax_client.py | 6 +++--- adalflow/tests/test_minimax_client.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/adalflow/adalflow/components/model_client/minimax_client.py b/adalflow/adalflow/components/model_client/minimax_client.py index bda0fd298..c35ee42aa 100644 --- a/adalflow/adalflow/components/model_client/minimax_client.py +++ b/adalflow/adalflow/components/model_client/minimax_client.py @@ -16,8 +16,8 @@ class MiniMaxClient(OpenAIClient): - The base URL to ``https://api.minimax.io/v1`` - The API key environment variable to ``MINIMAX_API_KEY`` -Available models include ``MiniMax-M2.7``, ``MiniMax-M2.5``, and ``MiniMax-M2.5-highspeed`` -(204K context window). +Available models include ``MiniMax-M3`` (default; 512K context, up to 128K output, image input), +``MiniMax-M2.7``, and ``MiniMax-M2.7-highspeed``. References: - To obtain your API key, sign up at: https://www.minimaxi.com/ @@ -36,7 +36,7 @@ class MiniMaxClient(OpenAIClient): generator = Generator( model_client=MiniMaxClient(), model_kwargs={ - "model": "MiniMax-M2.7", + "model": "MiniMax-M3", "temperature": 0.7, "stream": False, } diff --git a/adalflow/tests/test_minimax_client.py b/adalflow/tests/test_minimax_client.py index fada010d2..d5ae51a02 100644 --- a/adalflow/tests/test_minimax_client.py +++ b/adalflow/tests/test_minimax_client.py @@ -38,7 +38,7 @@ def setUp(self): self.api_kwargs = { "input": "What is the meaning of life?", - "model": "MiniMax-M2.7", + "model": "MiniMax-M3", } def test_minimax_client_init(self): @@ -173,7 +173,7 @@ def test_minimax_generator_integration(self, MockOpenAI, mock_init_sync_client): gen = Generator( model_client=client, model_kwargs={ - "model": "MiniMax-M2.7", + "model": "MiniMax-M3", "temperature": 0.7, "max_tokens": 1000, }, @@ -198,7 +198,7 @@ def test_minimax_convert_inputs_to_api_kwargs(self): api_kwargs = client.convert_inputs_to_api_kwargs( input="Hello, world!", model_kwargs={ - "model": "MiniMax-M2.7", + "model": "MiniMax-M3", "temperature": 0.7, }, model_type=ModelType.LLM, @@ -207,27 +207,27 @@ def test_minimax_convert_inputs_to_api_kwargs(self): # Verify the structure for Response API self.assertIn("input", api_kwargs) self.assertIn("model", api_kwargs) - self.assertEqual(api_kwargs["model"], "MiniMax-M2.7") + self.assertEqual(api_kwargs["model"], "MiniMax-M3") self.assertEqual(api_kwargs["temperature"], 0.7) # Verify input content self.assertEqual(api_kwargs["input"], "Hello, world!") - def test_minimax_convert_inputs_m25_highspeed(self): - """Test input conversion with MiniMax-M2.5-highspeed model.""" + def test_minimax_convert_inputs_m27_highspeed(self): + """Test input conversion with MiniMax-M2.7-highspeed model.""" with patch("os.getenv", side_effect=getenv_side_effect): client = MiniMaxClient(api_key="fake_api_key") api_kwargs = client.convert_inputs_to_api_kwargs( input="Summarize this text.", model_kwargs={ - "model": "MiniMax-M2.5-highspeed", + "model": "MiniMax-M2.7-highspeed", "temperature": 0.5, }, model_type=ModelType.LLM, ) - self.assertEqual(api_kwargs["model"], "MiniMax-M2.5-highspeed") + self.assertEqual(api_kwargs["model"], "MiniMax-M2.7-highspeed") self.assertEqual(api_kwargs["temperature"], 0.5) def test_minimax_from_dict_to_dict(self):