diff --git a/mem0/llms/aws_bedrock.py b/mem0/llms/aws_bedrock.py index 0db08589d..aa2c08101 100644 --- a/mem0/llms/aws_bedrock.py +++ b/mem0/llms/aws_bedrock.py @@ -17,8 +17,9 @@ from mem0.memory.utils import extract_json logger = logging.getLogger(__name__) PROVIDERS = [ - "ai21", "amazon", "anthropic", "cohere", "meta", "mistral", "stability", "writer", - "deepseek", "gpt-oss", "perplexity", "snowflake", "titan", "command", "j2", "llama" + "ai21", "amazon", "anthropic", "cohere", "meta", "mistral", "stability", "writer", + "deepseek", "gpt-oss", "perplexity", "snowflake", "titan", "command", "j2", "llama", + "minimax", ] @@ -120,6 +121,10 @@ class AWSBedrockLLM(LLMBase): """Initialize provider-specific settings and capabilities.""" # Determine capabilities based on provider and model self.supports_tools = self.provider in ["anthropic", "cohere", "amazon"] + # MiniMax M2.x is intentionally excluded from supports_tools: tool use for MiniMax + # on Amazon Bedrock is only available via the bedrock-mantle (OpenAI-compatible) + # endpoint, not via the bedrock-runtime Converse API used by this class. + # See: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-minimax-minimax-m2-5.html self.supports_vision = self.provider in ["anthropic", "amazon", "meta", "mistral"] self.supports_streaming = self.provider in ["anthropic", "cohere", "mistral", "amazon", "meta"] @@ -487,7 +492,12 @@ class AWSBedrockLLM(LLMBase): return 2000 def _build_inference_config(self) -> Dict[str, Any]: - """Build Converse ``inferenceConfig``. Anthropic allows only one of temperature or topP; we keep temperature and omit topP.""" + """Build Converse ``inferenceConfig``. + + Anthropic and MiniMax reasoning models reject requests that include both + ``temperature`` and ``topP`` simultaneously, so ``topP`` is omitted for + those providers even when the user has configured it. + """ inference_config: Dict[str, Any] = { "maxTokens": self.model_config.get("max_tokens", self._default_max_tokens_for_converse()), "temperature": self.model_config.get("temperature", 0.1), @@ -495,8 +505,11 @@ class AWSBedrockLLM(LLMBase): top_p = self.model_config.get("top_p") if top_p is not None: - if self.provider == "anthropic": - logger.debug("Omitting topP for Anthropic Converse (using temperature); top_p=%s", top_p) + if self.provider in ("anthropic", "minimax"): + # Both Anthropic and MiniMax M2.x (reasoning models) raise a + # ValidationException when temperature and topP are both present + # in inferenceConfig. Omit topP and rely on temperature only. + logger.debug("Omitting topP for %s Converse (using temperature); top_p=%s", self.provider, top_p) else: inference_config["topP"] = top_p @@ -568,6 +581,39 @@ class AWSBedrockLLM(LLMBase): else: return str(response) + elif self.provider == "minimax": + # MiniMax models (e.g. minimax.minimax-m2.5) use the Bedrock Converse API. + # M2.5 is a reasoning model whose response content array may include a + # `reasoningContent` block before the actual `text` block, so we iterate + # to find the first block that contains a "text" key. + # System messages must be passed via the top-level `system` parameter + # (not as a message with role="system") per the Converse API spec. + system_parts = [] + converse_messages = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + if not isinstance(content, str): + content = str(content) + if role == "system": + system_parts.append(content) + else: + converse_messages.append({"role": role, "content": [{"text": content}]}) + if not converse_messages: + converse_messages = [{"role": "user", "content": [{"text": ""}]}] + converse_params = { + "modelId": self.config.model, + "messages": converse_messages, + "inferenceConfig": self._build_inference_config(), + } + if system_parts: + converse_params["system"] = [{"text": "\n".join(system_parts)}] + response = self.client.converse(**converse_params) + for block in response["output"]["message"]["content"]: + if "text" in block: + return block["text"] + return "" + elif self.provider == "amazon" and "nova" in self.config.model.lower(): # Nova models use the Converse API even without tools formatted_messages = self._format_messages_amazon(messages) diff --git a/tests/llms/test_aws_bedrock.py b/tests/llms/test_aws_bedrock.py index efdc364d0..029c4a0b0 100644 --- a/tests/llms/test_aws_bedrock.py +++ b/tests/llms/test_aws_bedrock.py @@ -220,6 +220,25 @@ class TestBuildInferenceConfig: cfg = llm._build_inference_config() assert cfg["maxTokens"] == 2000 + def test_minimax_omits_top_p_when_explicitly_set(self, mock_boto3): + # MiniMax M2.x (reasoning model) rejects both temperature and topP simultaneously. + # Even when the user explicitly configures top_p, it must be omitted. + llm = _make_llm( + "minimax.minimax-m2.5", + mock_boto3, + temperature=0.1, + top_p=0.9, + ) + cfg = llm._build_inference_config() + assert "temperature" in cfg + assert "topP" not in cfg, "topP must be absent for MiniMax reasoning models" + + def test_minimax_only_temperature_by_default(self, mock_boto3): + llm = _make_llm("minimax.minimax-m2.5", mock_boto3, temperature=0.1) + cfg = llm._build_inference_config() + assert cfg["temperature"] == 0.1 + assert "topP" not in cfg + # --------------------------------------------------------------------------- # generate_response — Converse API call assertions @@ -328,3 +347,80 @@ class TestGenerateResponseConverse: _, kwargs = mock_boto3.converse.call_args assert "topP" not in kwargs["inferenceConfig"] + + +# --------------------------------------------------------------------------- +# MiniMax provider +# --------------------------------------------------------------------------- + +class TestMiniMaxProvider: + """Tests for MiniMax models via Bedrock Converse API.""" + + def test_extract_provider(self): + assert extract_provider("minimax.minimax-m2.5") == "minimax" + assert extract_provider("minimax.minimax-m2") == "minimax" + + def test_generate_response_text_only(self, mock_boto3): + """Standard response: single text block.""" + mock_boto3.converse.return_value = _converse_response("Hello!") + llm = _make_llm("minimax.minimax-m2.5", mock_boto3) + + result = llm.generate_response([{"role": "user", "content": "say hi"}]) + + assert result == "Hello!" + _, kwargs = mock_boto3.converse.call_args + assert kwargs["modelId"] == "minimax.minimax-m2.5" + assert kwargs["messages"][0]["role"] == "user" + assert kwargs["messages"][0]["content"][0]["text"] == "say hi" + + def test_generate_response_reasoning_model(self, mock_boto3): + """MiniMax M2.5 is a reasoning model: reasoningContent block comes before text.""" + reasoning_response = { + "output": { + "message": { + "content": [ + {"reasoningContent": {"reasoningText": {"text": "Let me think..."}}}, + {"text": "Hello!"}, + ] + } + } + } + mock_boto3.converse.return_value = reasoning_response + llm = _make_llm("minimax.minimax-m2.5", mock_boto3) + + result = llm.generate_response([{"role": "user", "content": "say hi"}]) + + # Must skip reasoningContent and return the actual text block + assert result == "Hello!" + + def test_inference_config(self, mock_boto3): + """inferenceConfig should include maxTokens and temperature; no topP.""" + mock_boto3.converse.return_value = _converse_response() + llm = _make_llm("minimax.minimax-m2.5", mock_boto3, temperature=0.2, max_tokens=512) + + llm.generate_response([{"role": "user", "content": "hi"}]) + + _, kwargs = mock_boto3.converse.call_args + assert kwargs["inferenceConfig"]["maxTokens"] == 512 + assert kwargs["inferenceConfig"]["temperature"] == 0.2 + assert "topP" not in kwargs["inferenceConfig"] + + def test_system_prompt_passed_correctly(self, mock_boto3): + """System messages must be sent via top-level `system` param, not as a message role.""" + mock_boto3.converse.return_value = _converse_response('{"facts": ["test"]}') + llm = _make_llm("minimax.minimax-m2.5", mock_boto3) + + llm.generate_response([ + {"role": "system", "content": "Return JSON only."}, + {"role": "user", "content": "Extract facts from: test"}, + ]) + + _, kwargs = mock_boto3.converse.call_args + # system prompt must be in top-level "system" key + assert "system" in kwargs + assert kwargs["system"][0]["text"] == "Return JSON only." + # messages list must NOT contain a system role entry + for msg in kwargs["messages"]: + assert msg["role"] != "system" + # user message must be present + assert kwargs["messages"][0]["role"] == "user"