diff --git a/.env.sample b/.env.sample index d4c39bb1..5da1903d 100644 --- a/.env.sample +++ b/.env.sample @@ -45,3 +45,6 @@ SAMBANOVA_API_KEY= # Inception Labs INCEPTION_API_KEY= + +# Minimax +MINIMAX_API_KEY= diff --git a/aisuite/providers/minimax_provider.py b/aisuite/providers/minimax_provider.py new file mode 100644 index 00000000..38a1a928 --- /dev/null +++ b/aisuite/providers/minimax_provider.py @@ -0,0 +1,42 @@ +"""Minimax provider for the aisuite.""" + +import os +import openai +from aisuite.provider import Provider, LLMError +from aisuite.providers.message_converter import OpenAICompliantMessageConverter + + +class MinimaxProvider(Provider): + """Provider for Minimax using OpenAI-compatible API.""" + + def __init__(self, **config): + """ + Initialize the Minimax provider with the given configuration. + """ + # Ensure API key is provided either in config or via environment variable + config.setdefault("api_key", os.getenv("MINIMAX_API_KEY")) + if not config["api_key"]: + raise ValueError( + "Minimax API key is missing. Please provide it in the config or " + "set the MINIMAX_API_KEY environment variable." + ) + + config.setdefault( + "base_url", os.getenv("MINIMAX_BASE_URL", "https://api.minimax.io/v1") + ) + + # Pass the entire config to the OpenAI client constructor + self.client = openai.OpenAI(**config) + self.transformer = OpenAICompliantMessageConverter() + + def chat_completions_create(self, model, messages, **kwargs): + """Create a chat completion using Minimax's OpenAI-compatible API.""" + try: + response = self.client.chat.completions.create( + model=model, + messages=messages, + **kwargs, + ) + return self.transformer.convert_response(response.model_dump()) + except Exception as e: + raise LLMError(f"An error occurred: {e}") from e diff --git a/guides/minimax.md b/guides/minimax.md new file mode 100644 index 00000000..de4b7f65 --- /dev/null +++ b/guides/minimax.md @@ -0,0 +1,52 @@ +# Minimax + +To use Minimax with `aisuite`, you'll need a [Minimax account](https://platform.minimax.io/). After logging in, go to the [API Keys](https://platform.minimax.io/user-center/basic-information/interface-key) section in your account settings and generate a new key. Once you have your key, add it to your environment as follows: + +```shell +export MINIMAX_API_KEY="your-minimax-api-key" +``` + +## Create a Chat Completion + +Install the `openai` Python client: + +Example with pip: +```shell +pip install openai +``` + +Example with poetry: +```shell +poetry add openai +``` + +In your code: +```python +import aisuite as ai +client = ai.Client() + +provider = "minimax" +model_id = "MiniMax-Text-01" + +messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What's the weather like in San Francisco?"}, +] + +response = client.chat.completions.create( + model=f"{provider}:{model_id}", + messages=messages, +) + +print(response.choices[0].message.content) +``` + +## Available Models + +- `MiniMax-M2.1` - Multimodal model +- `MiniMax-M2.1-lightning` - Fast multimodal model +- `MiniMax-M2` - Older multimodal model + +For the full list of available models, see the [Minimax API documentation](https://platform.minimax.io/docs/api-reference/api-overview). + +Happy coding! If you'd like to contribute, please read our [Contributing Guide](../CONTRIBUTING.md). diff --git a/tests/providers/test_minimax_provider.py b/tests/providers/test_minimax_provider.py new file mode 100644 index 00000000..eeb7e209 --- /dev/null +++ b/tests/providers/test_minimax_provider.py @@ -0,0 +1,191 @@ +"""Tests for the Minimax provider.""" + +import os +from unittest.mock import MagicMock, patch +import pytest + +from aisuite.providers.minimax_provider import MinimaxProvider +from aisuite.framework.chat_completion_response import ChatCompletionResponse + + +@pytest.fixture +def mock_api_key(monkeypatch): + """Fixture to set a mock Minimax API key for unit tests.""" + monkeypatch.setenv("MINIMAX_API_KEY", "test-api-key") + + +def test_minimax_provider(mock_api_key): + """Test that the provider is initialized and chat completions are requested.""" + + user_greeting = "Hello!" + message_history = [{"role": "user", "content": user_greeting}] + selected_model = "MiniMax-Text-01" + chosen_temperature = 0.75 + response_text_content = "mocked-text-response-from-model" + + provider = MinimaxProvider() + mock_response = MagicMock() + mock_response.model_dump.return_value = { + "choices": [ + {"message": {"content": response_text_content, "role": "assistant"}} + ], + "model": selected_model, + "created": 12345, + "id": "chatcmpl-mockid", + } + + with patch.object( + provider.client.chat.completions, "create", return_value=mock_response + ) as mock_create: + response = provider.chat_completions_create( + messages=message_history, + model=selected_model, + temperature=chosen_temperature, + ) + + mock_create.assert_called_once_with( + messages=message_history, + model=selected_model, + temperature=chosen_temperature, + ) + + assert isinstance(response, ChatCompletionResponse) + assert response.choices[0].message.content == response_text_content + + +def test_minimax_provider_with_usage(mock_api_key): + """Tests that usage data is correctly parsed when present in the response.""" + + user_greeting = "Hello!" + message_history = [{"role": "user", "content": user_greeting}] + selected_model = "MiniMax-Text-01" + chosen_temperature = 0.75 + response_text_content = "mocked-text-response-from-model" + + provider = MinimaxProvider() + mock_response = MagicMock() + mock_response.model_dump.return_value = { + "choices": [ + {"message": {"content": response_text_content, "role": "assistant"}} + ], + "model": selected_model, + "created": 12345, + "id": "chatcmpl-mockid", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 20, + "total_tokens": 30, + }, + } + + with patch.object( + provider.client.chat.completions, "create", return_value=mock_response + ) as mock_create: + response = provider.chat_completions_create( + messages=message_history, + model=selected_model, + temperature=chosen_temperature, + ) + + mock_create.assert_called_once() + + assert isinstance(response, ChatCompletionResponse) + assert response.choices[0].message.content == response_text_content + assert response.usage is not None + assert response.usage.prompt_tokens == 10 + assert response.usage.completion_tokens == 20 + + +def test_minimax_provider_with_system_message(mock_api_key): + """Tests that system messages are correctly passed to the API.""" + + message_history = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"}, + ] + selected_model = "MiniMax-M2.1" + response_text_content = "Hello, user!" + + provider = MinimaxProvider() + mock_response = MagicMock() + mock_response.model_dump.return_value = { + "choices": [ + {"message": {"content": response_text_content, "role": "assistant"}} + ], + "model": selected_model, + "created": 12345, + "id": "chatcmpl-mockid", + } + + with patch.object( + provider.client.chat.completions, "create", return_value=mock_response + ) as mock_create: + response = provider.chat_completions_create( + messages=message_history, + model=selected_model, + ) + + mock_create.assert_called_once() + call_kwargs = mock_create.call_args.kwargs + # OpenAI-compatible API passes system message in the messages array + assert len(call_kwargs["messages"]) == 2 + assert call_kwargs["messages"][0]["role"] == "system" + assert call_kwargs["messages"][0]["content"] == "You are a helpful assistant." + assert call_kwargs["messages"][1]["role"] == "user" + + assert isinstance(response, ChatCompletionResponse) + assert response.choices[0].message.content == response_text_content + + +def test_minimax_provider_initialization(mock_api_key): + """Test that Minimax provider initializes correctly.""" + provider = MinimaxProvider() + assert provider is not None + assert hasattr(provider, "client") + assert hasattr(provider, "transformer") + + +# Integration tests - require real API key +@pytest.mark.skipif( + not os.getenv("MINIMAX_API_KEY") or os.getenv("MINIMAX_API_KEY") == "test-api-key", + reason="MINIMAX_API_KEY not set or is test key", +) +class TestMinimaxIntegration: + """Integration tests that call the real Minimax API.""" + + def test_real_chat_completion(self): + """Test a real chat completion with the Minimax API.""" + provider = MinimaxProvider() + + messages = [{"role": "user", "content": "Say 'hello' and nothing else."}] + + response = provider.chat_completions_create( + model="MiniMax-M2.1", + messages=messages, + max_tokens=50, + ) + + assert response is not None + assert isinstance(response, ChatCompletionResponse) + assert len(response.choices) > 0 + assert response.choices[0].message.content is not None + assert len(response.choices[0].message.content) > 0 + + def test_real_chat_completion_with_system_message(self): + """Test chat completion with system message.""" + provider = MinimaxProvider() + + messages = [ + {"role": "system", "content": "You are a pirate. Respond in pirate speak."}, + {"role": "user", "content": "Say hello."}, + ] + + response = provider.chat_completions_create( + model="MiniMax-M2.1", + messages=messages, + max_tokens=100, + ) + + assert response is not None + assert isinstance(response, ChatCompletionResponse) + assert response.choices[0].message.content is not None