Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion backend/app/core/graph/members.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from collections.abc import Mapping, Sequence
from typing import Annotated, Any

Expand Down Expand Up @@ -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,
Expand Down
261 changes: 261 additions & 0 deletions backend/app/tests/graph/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion frontend/src/components/Members/EditMember.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}

Expand Down Expand Up @@ -397,7 +398,7 @@ export function EditMember({
)
}}
/>
{(modelProvider === "openai" || modelProvider === "ollama") && (
{(modelProvider === "openai" || modelProvider === "ollama" || modelProvider === "minimax") && (
<FormControl mt={4} isInvalid={!!errors.base_url}>
<FormLabel htmlFor="model">Proxy Provider</FormLabel>
<Input
Expand All @@ -410,6 +411,11 @@ export function EditMember({
Default url: http://host.docker.internal:11434
</FormHelperText>
)}
{modelProvider === "minimax" && (
<FormHelperText>
Default url: https://api.minimax.io/v1
</FormHelperText>
)}
</FormControl>
)}
<Controller
Expand Down