diff --git a/.env.example b/.env.example index aafa5c9c..8721055d 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,7 @@ RECURSION_LIMIT=25 # llm provider keys. Add only to models that you want to use OPENAI_API_KEY= ANTHROPIC_API_KEY= +MINIMAX_API_KEY= # Embedding model. See the list of supported models: https://qdrant.github.io/fastembed/examples/Supported_Models/ DENSE_EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 diff --git a/README.md b/README.md index b1aaaa13..fa10cd00 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ - [Retrieval Augmented Generation (RAG)](#retrieval-augmented-generation-rag) - [Customising embedding models](#customising-embedding-models) - [Using Open Source Models](#using-open-source-models) + - [Using MiniMax Models](#using-minimax-models) - [Using Open Source Models with Ollama](#using-open-source-models-with-ollama) - [Choosing the Right Models](#choosing-the-right-models) - [Using Open Source Models without Ollama](#using-open-source-models-without-ollama) @@ -210,6 +211,18 @@ DENSE_EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 # Change this > [!WARNING] > If your existing and new embedding models have different vector dimensions, you may need to recreate your Qdrant collection. You can delete the collection through the Qdrant Dashboard at [http://qdrant.localhost/dashboard](http://qdrant.localhost/dashboard). Therefore, it is better to plan ahead which embedding model is most suitable for your workflows. +### Using MiniMax Models + +[MiniMax](https://www.minimax.io/) offers powerful cloud-based LLM models with an OpenAI-compatible API. Available models include: + +- **MiniMax-M2.7** — Latest flagship model with 1M context window +- **MiniMax-M2.7-highspeed** — Faster variant for lower-latency use cases + +To use MiniMax: +1. **Get an API key:** Sign up at [MiniMax](https://www.minimax.io/) and obtain your API key. +2. **Set the environment variable:** Add `MINIMAX_API_KEY=your_key` to your `.env` file. +3. **Configure your agents:** Select `minimax` as the provider and choose a model from the dropdown. + ### Using Open Source Models Open source models are becoming cheaper and easier to run, and some even match the performance of closed models. You might prefer using them for their privacy and cost benefits. If you are running Tribe locally and want to use open source models, I would recommend Ollama for its ease of use. diff --git a/backend/app/core/graph/members.py b/backend/app/core/graph/members.py index ee99de36..d07ea809 100644 --- a/backend/app/core/graph/members.py +++ b/backend/app/core/graph/members.py @@ -1,3 +1,4 @@ +import os from collections.abc import Mapping, Sequence from typing import Annotated, Any @@ -157,7 +158,17 @@ def __init__( ): # If using proxy, then we need to pass base url # TODO: Include ollama here once langchain-ollama bug is fixed - if provider in ["openai"] and base_url: + if provider == "minimax": + # MiniMax uses an OpenAI-compatible API at api.minimax.io + minimax_temperature = max(0.01, min(temperature, 1.0)) + self.model = ChatOpenAI( + model=model, + temperature=minimax_temperature, + base_url=base_url or "https://api.minimax.io/v1", + api_key=os.environ.get("MINIMAX_API_KEY", ""), + streaming=True, + ) + elif provider in ["openai"] and base_url: self.model = init_chat_model( model, model_provider=provider, diff --git a/backend/app/tests/graph/test_minimax_provider.py b/backend/app/tests/graph/test_minimax_provider.py new file mode 100644 index 00000000..9d3b9d6e --- /dev/null +++ b/backend/app/tests/graph/test_minimax_provider.py @@ -0,0 +1,261 @@ +"""Tests for MiniMax LLM provider integration.""" + +import os +from unittest.mock import patch + +import pytest +from langchain_openai import ChatOpenAI + +from app.core.graph.members import BaseNode, GraphMember, GraphTeam + + +class TestMiniMaxProviderInit: + """Unit tests for MiniMax provider initialization in BaseNode.""" + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_creates_chat_openai(self) -> None: + """MiniMax provider should create a ChatOpenAI instance.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert isinstance(node.model, ChatOpenAI) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_default_base_url(self) -> None: + """MiniMax provider should use default base_url when none is provided.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert isinstance(node.model, ChatOpenAI) + assert str(node.model.openai_api_base) == "https://api.minimax.io/v1" + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_custom_base_url(self) -> None: + """MiniMax provider should use custom base_url when provided.""" + custom_url = "https://custom-proxy.example.com/v1" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=custom_url, + temperature=0.7, + ) + assert isinstance(node.model, ChatOpenAI) + assert str(node.model.openai_api_base) == custom_url + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_model_name(self) -> None: + """MiniMax provider should pass the correct model name.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert node.model.model_name == "MiniMax-M2.7" + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_highspeed_model(self) -> None: + """MiniMax provider should work with highspeed model variant.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7-highspeed", + base_url=None, + temperature=0.5, + ) + assert node.model.model_name == "MiniMax-M2.7-highspeed" + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_streaming_enabled(self) -> None: + """MiniMax provider should have streaming enabled.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert node.model.streaming is True + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_provider_api_key_from_env(self) -> None: + """MiniMax provider should read API key from MINIMAX_API_KEY env var.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert isinstance(node.model, ChatOpenAI) + assert node.model.openai_api_key.get_secret_value() == "test-minimax-key-123" + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_final_answer_model_same_as_model(self) -> None: + """final_answer_model should be the same as model for MiniMax provider.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert node.final_answer_model is node.model + + +class TestMiniMaxTemperatureClamping: + """Unit tests for MiniMax temperature clamping behavior.""" + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_temperature_zero_clamped_to_min(self) -> None: + """Temperature of 0.0 should be clamped to 0.01 for MiniMax.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.0, + ) + assert node.model.temperature == pytest.approx(0.01) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_temperature_normal_value(self) -> None: + """Normal temperature values should pass through unchanged.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert node.model.temperature == pytest.approx(0.7) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_temperature_max_boundary(self) -> None: + """Temperature of 1.0 should be accepted as-is.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=1.0, + ) + assert node.model.temperature == pytest.approx(1.0) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_temperature_above_max_clamped(self) -> None: + """Temperature above 1.0 should be clamped to 1.0.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=1.5, + ) + assert node.model.temperature == pytest.approx(1.0) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_temperature_negative_clamped(self) -> None: + """Negative temperature should be clamped to 0.01.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=-0.5, + ) + assert node.model.temperature == pytest.approx(0.01) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-minimax-key-123"}) + def test_minimax_temperature_small_positive(self) -> None: + """Small positive temperature should pass through.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.1, + ) + assert node.model.temperature == pytest.approx(0.1) + + +class TestMiniMaxGraphModels: + """Unit tests for MiniMax with GraphMember and GraphTeam models.""" + + def test_graph_member_minimax_provider(self) -> None: + """GraphMember should accept minimax as provider.""" + member = GraphMember( + name="test-member", + role="researcher", + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + backstory="A research assistant", + tools=[], + interrupt=False, + ) + assert member.provider == "minimax" + assert member.model == "MiniMax-M2.7" + + def test_graph_team_minimax_provider(self) -> None: + """GraphTeam should accept minimax as provider.""" + team = GraphTeam( + name="test-team", + role="coordinator", + backstory="A team coordinator", + members={}, + provider="minimax", + model="MiniMax-M2.7-highspeed", + base_url=None, + temperature=0.5, + ) + assert team.provider == "minimax" + assert team.model == "MiniMax-M2.7-highspeed" + + +class TestMiniMaxProviderIntegration: + """Integration tests for MiniMax provider (require MINIMAX_API_KEY).""" + + @pytest.mark.skipif( + not os.environ.get("MINIMAX_API_KEY"), + reason="MINIMAX_API_KEY not set", + ) + def test_minimax_provider_real_init(self) -> None: + """Integration test: MiniMax provider initializes with real API key.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7", + base_url=None, + temperature=0.7, + ) + assert isinstance(node.model, ChatOpenAI) + assert node.model.model_name == "MiniMax-M2.7" + + @pytest.mark.skipif( + not os.environ.get("MINIMAX_API_KEY"), + reason="MINIMAX_API_KEY not set", + ) + def test_minimax_highspeed_real_init(self) -> None: + """Integration test: MiniMax highspeed model initializes correctly.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7-highspeed", + base_url=None, + temperature=0.5, + ) + assert isinstance(node.model, ChatOpenAI) + assert node.model.model_name == "MiniMax-M2.7-highspeed" + + @pytest.mark.skipif( + not os.environ.get("MINIMAX_API_KEY"), + reason="MINIMAX_API_KEY not set", + ) + @pytest.mark.asyncio + async def test_minimax_provider_basic_invoke(self) -> None: + """Integration test: MiniMax provider can make a basic API call.""" + node = BaseNode( + provider="minimax", + model="MiniMax-M2.7-highspeed", + base_url=None, + temperature=0.7, + ) + response = await node.model.ainvoke("Say 'hello' in one word.") + assert response.content + assert len(response.content) > 0 diff --git a/frontend/src/components/Members/EditMember.tsx b/frontend/src/components/Members/EditMember.tsx index c599e49e..ee40f6c6 100644 --- a/frontend/src/components/Members/EditMember.tsx +++ b/frontend/src/components/Members/EditMember.tsx @@ -78,6 +78,7 @@ const AVAILABLE_MODELS = { "claude-3-sonnet-20240229", "claude-3-opus-20240229", ], + minimax: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"], ollama: ["llama3.1"], } @@ -397,7 +398,7 @@ export function EditMember({ ) }} /> - {(modelProvider === "openai" || modelProvider === "ollama") && ( + {(modelProvider === "openai" || modelProvider === "ollama" || modelProvider === "minimax") && ( Proxy Provider )} + {modelProvider === "minimax" && ( + + Default url: https://api.minimax.io/v1 + + )} )}