diff --git a/docs/griptape-framework/drivers/prompt-drivers.md b/docs/griptape-framework/drivers/prompt-drivers.md index dfe3844fc..5b6868732 100644 --- a/docs/griptape-framework/drivers/prompt-drivers.md +++ b/docs/griptape-framework/drivers/prompt-drivers.md @@ -370,6 +370,54 @@ The [GrokPromptDriver](../../reference/griptape/drivers/prompt/grok_prompt_drive --8<-- "docs/griptape-framework/drivers/logs/prompt_drivers_grok.txt" ``` +### MiniMax + +The [MinimaxPromptDriver](../../reference/griptape/drivers/prompt/minimax_prompt_driver.md) connects to the [MiniMax OpenAI-compatible API](https://platform.minimax.io/docs/api-reference/text-openai-api). It supports `MiniMax-M3` and `MiniMax-M2.7` and uses the global endpoint by default: + +```python +import os + +from griptape.drivers.prompt.minimax import MinimaxPromptDriver + +driver = MinimaxPromptDriver(api_key=os.environ["MINIMAX_API_KEY"], model="MiniMax-M3") +``` + +`MiniMax-M3` enables thinking by default. Pass MiniMax-specific request fields through the OpenAI SDK's `extra_body` parameter to control it: + +```python +driver = MinimaxPromptDriver( + api_key=os.environ["MINIMAX_API_KEY"], + model="MiniMax-M3", + extra_params={"extra_body": {"thinking": {"type": "disabled"}}}, +) +``` + +Use `"adaptive"` instead of `"disabled"` to explicitly enable `MiniMax-M3` thinking. `MiniMax-M2.7` always uses thinking, even when `"disabled"` is requested. + +This driver supports text and image prompt content for `MiniMax-M3`; `MiniMax-M2.7` supports text only. The MiniMax API also accepts video input for `MiniMax-M3`, but Griptape prompt content does not currently expose video input. + +For the China endpoint, set `base_url="https://api.minimaxi.com/v1"`. + +MiniMax also provides an [Anthropic-compatible API](https://platform.minimax.io/docs/api-reference/text-anthropic-api). Configure an `AnthropicPromptDriver` with an Anthropic client and the MiniMax tokenizer: + +```python +import os + +from anthropic import Anthropic + +from griptape.drivers.prompt.anthropic import AnthropicPromptDriver +from griptape.tokenizers import MinimaxTokenizer + +model = "MiniMax-M3" +driver = AnthropicPromptDriver( + client=Anthropic(api_key=os.environ["MINIMAX_API_KEY"], base_url="https://api.minimax.io/anthropic"), + model=model, + tokenizer=MinimaxTokenizer(model=model), +) +``` + +This configuration requires the `drivers-prompt-anthropic` extra. For the China endpoint, set the Anthropic client's `base_url` to `https://api.minimaxi.com/anthropic`. + ### Perplexity The [PerplexityPromptDriver](../../reference/griptape/drivers/prompt/perplexity_prompt_driver.md) uses [Perplexity Sonar's chat completion](https://docs.perplexity.ai/api-reference/chat-completions) endpoint. diff --git a/griptape/drivers/__init__.py b/griptape/drivers/__init__.py index 4fba24367..21960cbf6 100644 --- a/griptape/drivers/__init__.py +++ b/griptape/drivers/__init__.py @@ -14,6 +14,7 @@ from .prompt.dummy import DummyPromptDriver from .prompt.ollama import OllamaPromptDriver from .prompt.grok import GrokPromptDriver +from .prompt.minimax import MinimaxPromptDriver from .prompt.griptape_cloud import GriptapeCloudPromptDriver from .prompt.perplexity import PerplexityPromptDriver @@ -215,6 +216,7 @@ "LocalVectorStoreDriver", "MarkdownifyWebScraperDriver", "MarqoVectorStoreDriver", + "MinimaxPromptDriver", "MongoDbAtlasVectorStoreDriver", "NoOpObservabilityDriver", "OllamaEmbeddingDriver", diff --git a/griptape/drivers/prompt/minimax/__init__.py b/griptape/drivers/prompt/minimax/__init__.py new file mode 100644 index 000000000..e7e8dd554 --- /dev/null +++ b/griptape/drivers/prompt/minimax/__init__.py @@ -0,0 +1,5 @@ +from griptape.drivers.prompt.minimax_prompt_driver import MinimaxPromptDriver + +__all__ = [ + "MinimaxPromptDriver", +] diff --git a/griptape/drivers/prompt/minimax_prompt_driver.py b/griptape/drivers/prompt/minimax_prompt_driver.py new file mode 100644 index 000000000..36afedb05 --- /dev/null +++ b/griptape/drivers/prompt/minimax_prompt_driver.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from attrs import Factory, define, field + +from griptape.drivers.prompt.openai import OpenAiChatPromptDriver +from griptape.tokenizers.minimax_tokenizer import MinimaxTokenizer + + +@define +class MinimaxPromptDriver(OpenAiChatPromptDriver): + base_url: str = field(default="https://api.minimax.io/v1", kw_only=True, metadata={"serializable": True}) + tokenizer: MinimaxTokenizer = field( + default=Factory(lambda self: MinimaxTokenizer(model=self.model), takes_self=True), + kw_only=True, + metadata={"serializable": True}, + ) diff --git a/griptape/tokenizers/__init__.py b/griptape/tokenizers/__init__.py index 6fdc7f71c..94b383ca0 100644 --- a/griptape/tokenizers/__init__.py +++ b/griptape/tokenizers/__init__.py @@ -9,6 +9,7 @@ from griptape.tokenizers.dummy_tokenizer import DummyTokenizer from griptape.tokenizers.amazon_bedrock_tokenizer import AmazonBedrockTokenizer from griptape.tokenizers.grok_tokenizer import GrokTokenizer +from griptape.tokenizers.minimax_tokenizer import MinimaxTokenizer __all__ = [ @@ -20,6 +21,7 @@ "GoogleTokenizer", "GrokTokenizer", "HuggingFaceTokenizer", + "MinimaxTokenizer", "OpenAiTokenizer", "SimpleTokenizer", "VoyageAiTokenizer", diff --git a/griptape/tokenizers/minimax_tokenizer.py b/griptape/tokenizers/minimax_tokenizer.py new file mode 100644 index 000000000..5eaba266a --- /dev/null +++ b/griptape/tokenizers/minimax_tokenizer.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from attrs import define + +from griptape.tokenizers import OpenAiTokenizer + + +@define() +class MinimaxTokenizer(OpenAiTokenizer): + # The OpenAI-compatible API does not expose token counting, so this tokenizer + # uses local tiktoken logic. + # https://platform.minimax.io/docs/api-reference/api-overview + MODEL_PREFIXES_TO_MAX_INPUT_TOKENS = { + "MiniMax-M3": 1_000_000, + "MiniMax-M2.7": 204_800, + } + MODEL_PREFIXES_TO_MAX_OUTPUT_TOKENS = { + "MiniMax-M3": 524_288, + "MiniMax-M2.7": 204_800, + } diff --git a/tests/unit/drivers/prompt/test_minimax_prompt_driver.py b/tests/unit/drivers/prompt/test_minimax_prompt_driver.py new file mode 100644 index 000000000..c7c1a31c9 --- /dev/null +++ b/tests/unit/drivers/prompt/test_minimax_prompt_driver.py @@ -0,0 +1,68 @@ +import pytest + +from griptape.drivers.prompt.minimax import MinimaxPromptDriver +from tests.unit.drivers.prompt.test_openai_chat_prompt_driver import TestOpenAiChatPromptDriverFixtureMixin + + +class TestMinimaxPromptDriver(TestOpenAiChatPromptDriverFixtureMixin): + @pytest.mark.parametrize("model", ["MiniMax-M3", "MiniMax-M2.7"]) + def test_init(self, model): + assert MinimaxPromptDriver(api_key="foo", model=model) + + def test_default_base_url(self): + driver = MinimaxPromptDriver(api_key="foo", model="MiniMax-M3") + + assert driver.base_url == "https://api.minimax.io/v1" + assert driver.tokenizer.model == "MiniMax-M3" + + def test_custom_base_url(self): + driver = MinimaxPromptDriver( + api_key="foo", + model="MiniMax-M3", + base_url="https://api.minimaxi.com/v1", + ) + + assert driver.base_url == "https://api.minimaxi.com/v1" + + def test_to_dict(self): + # Given + driver = MinimaxPromptDriver(model="MiniMax-M3") + + # When + result = driver.to_dict() + + # Then + assert result == { + "type": "MinimaxPromptDriver", + "audio": {"format": "pcm16", "voice": "alloy"}, + "base_url": "https://api.minimax.io/v1", + "extra_params": {}, + "max_tokens": None, + "model": "MiniMax-M3", + "modalities": [], + "organization": None, + "parallel_tool_calls": True, + "reasoning_effort": "medium", + "response_format": None, + "seed": None, + "stream": False, + "structured_output_strategy": "native", + "temperature": 0.1, + "tokenizer": { + "type": "MinimaxTokenizer", + "model": "MiniMax-M3", + "stop_sequences": [], + }, + "use_native_tools": True, + "user": "", + } + + def test_from_dict(self): + # Given + driver = MinimaxPromptDriver(model="MiniMax-M3") + + # When + result = MinimaxPromptDriver.from_dict(driver.to_dict()) + + # Then + assert result.to_dict() == driver.to_dict() diff --git a/tests/unit/tokenizers/test_minimax_tokenizer.py b/tests/unit/tokenizers/test_minimax_tokenizer.py new file mode 100644 index 000000000..07f039344 --- /dev/null +++ b/tests/unit/tokenizers/test_minimax_tokenizer.py @@ -0,0 +1,26 @@ +import pytest + +from griptape.tokenizers import MinimaxTokenizer + + +class TestMinimaxTokenizer: + def test_init(self): + assert MinimaxTokenizer(model="MiniMax-M3") + + def test_count_tokens(self): + tokenizer = MinimaxTokenizer(model="MiniMax-M3") + + assert tokenizer.count_tokens("foo bar huzzah") == 5 + + @pytest.mark.parametrize( + ("model", "context_window", "max_output_tokens"), + [ + ("MiniMax-M3", 1_000_000, 524_288), + ("MiniMax-M2.7", 204_800, 204_800), + ], + ) + def test_token_limits(self, model, context_window, max_output_tokens): + tokenizer = MinimaxTokenizer(model=model) + + assert tokenizer.max_input_tokens == context_window - MinimaxTokenizer.TOKEN_OFFSET + assert tokenizer.max_output_tokens == max_output_tokens