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
5 changes: 5 additions & 0 deletions py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ pip install r2r
export OPENAI_API_KEY=sk-...
python -m r2r.serve

# Or run with MiniMax as the LLM provider
# export MINIMAX_API_KEY=your-key
# export R2R_CONFIG_NAME=minimax
# python -m r2r.serve

# Or run in full mode with Docker
# git clone git@github.com:SciPhi-AI/R2R.git && cd R2R
# export R2R_CONFIG_NAME=full OPENAI_API_KEY=sk-...
Expand Down
17 changes: 17 additions & 0 deletions py/core/configs/minimax.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[app]
# MiniMax models via OpenAI-compatible API (https://api.minimax.io/v1)
# Requires MINIMAX_API_KEY environment variable
fast_llm = "minimax/MiniMax-M2.5-highspeed"
quality_llm = "minimax/MiniMax-M2.7"
vlm = "minimax/MiniMax-M2.7"
audio_lm = "minimax/MiniMax-M2.7"

[completion]
provider = "openai"
concurrent_request_limit = 256

[completion.generation_config]
temperature = 0.1
top_p = 1
max_tokens_to_sample = 4096
stream = false
45 changes: 43 additions & 2 deletions py/core/providers/llm/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def __init__(self, config: CompletionConfig, *args, **kwargs) -> None:
self.async_ollama_client = None
self.lmstudio_client = None
self.async_lmstudio_client = None
self.minimax_client = None
self.async_minimax_client = None
# NEW: Azure Foundry clients using the Azure Inference API
self.azure_foundry_client = None
self.async_azure_foundry_client = None
Expand Down Expand Up @@ -101,6 +103,22 @@ def __init__(self, config: CompletionConfig, *args, **kwargs) -> None:
)
logger.debug("LMStudio OpenAI clients initialized successfully")

# Initialize MiniMax clients if credentials exist
minimax_api_key = os.getenv("MINIMAX_API_KEY")
minimax_api_base = os.getenv(
"MINIMAX_API_BASE", "https://api.minimax.io/v1"
)
if minimax_api_key:
self.minimax_client = OpenAI(
api_key=minimax_api_key,
base_url=minimax_api_base,
)
self.async_minimax_client = AsyncOpenAI(
api_key=minimax_api_key,
base_url=minimax_api_base,
)
logger.debug("MiniMax clients initialized successfully")

# Initialize Azure Foundry clients if credentials exist.
# These use the Azure Inference API (currently pasted into this handler).
azure_foundry_api_key = os.getenv("AZURE_FOUNDRY_API_KEY")
Expand Down Expand Up @@ -136,13 +154,15 @@ def __init__(self, config: CompletionConfig, *args, **kwargs) -> None:
self.azure_client,
self.ollama_client,
self.lmstudio_client,
self.minimax_client,
self.azure_foundry_client,
]
):
raise ValueError(
"No valid client credentials found. Please set either OPENAI_API_KEY, "
"both AZURE_API_KEY and AZURE_API_BASE environment variables, "
"OLLAMA_API_BASE, LMSTUDIO_API_BASE, or AZURE_FOUNDRY_API_KEY and AZURE_FOUNDRY_API_ENDPOINT."
"OLLAMA_API_BASE, LMSTUDIO_API_BASE, MINIMAX_API_KEY, "
"or AZURE_FOUNDRY_API_KEY and AZURE_FOUNDRY_API_ENDPOINT."
)

def _get_client_and_model(self, model: str):
Expand Down Expand Up @@ -178,6 +198,12 @@ def _get_client_and_model(self, model: str):
"LMStudio credentials not configured but lmstudio/ model prefix used"
)
return self.lmstudio_client, model[9:] # Strip 'lmstudio/' prefix
elif model.startswith("minimax/"):
if not self.minimax_client:
raise ValueError(
"MiniMax credentials not configured but minimax/ model prefix used"
)
return self.minimax_client, model[8:] # Strip 'minimax/' prefix
elif model.startswith("azure-foundry/"):
if not self.azure_foundry_client:
raise ValueError(
Expand All @@ -197,6 +223,8 @@ def _get_client_and_model(self, model: str):
return self.ollama_client, model
elif self.lmstudio_client:
return self.lmstudio_client, model
elif self.minimax_client:
return self.minimax_client, model
elif self.azure_foundry_client:
return self.azure_foundry_client, model
else:
Expand Down Expand Up @@ -234,6 +262,12 @@ def _get_async_client_and_model(self, model: str):
"LMStudio credentials not configured but lmstudio/ model prefix used"
)
return self.async_lmstudio_client, model[9:]
elif model.startswith("minimax/"):
if not self.async_minimax_client:
raise ValueError(
"MiniMax credentials not configured but minimax/ model prefix used"
)
return self.async_minimax_client, model[8:]
elif model.startswith("azure-foundry/"):
if not self.async_azure_foundry_client:
raise ValueError(
Expand All @@ -249,6 +283,8 @@ def _get_async_client_and_model(self, model: str):
return self.async_ollama_client, model
elif self.async_lmstudio_client:
return self.async_lmstudio_client, model
elif self.async_minimax_client:
return self.async_minimax_client, model
elif self.async_azure_foundry_client:
return self.async_azure_foundry_client, model
else:
Expand Down Expand Up @@ -398,6 +434,7 @@ def _get_base_args(self, generation_config: GenerationConfig) -> dict:
}

model_str = generation_config.model or ""
is_minimax = model_str.startswith("minimax/")

if any(
model_prefix in model_str.lower()
Expand All @@ -409,7 +446,11 @@ def _get_base_args(self, generation_config: GenerationConfig) -> dict:

else:
args["max_tokens"] = generation_config.max_tokens_to_sample
args["temperature"] = generation_config.temperature
temperature = generation_config.temperature
# MiniMax API requires temperature in [0, 1]
if is_minimax:
temperature = max(0.0, min(1.0, temperature))
args["temperature"] = temperature
args["top_p"] = generation_config.top_p

if generation_config.reasoning_effort is not None:
Expand Down
1 change: 1 addition & 0 deletions py/core/providers/llm/r2r_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def _choose_subprovider_by_model(
"deepseek/",
"ollama/",
"lmstudio/",
"minimax/",
]
if (
any(
Expand Down
108 changes: 108 additions & 0 deletions py/tests/integration/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Integration tests for MiniMax LLM provider.

These tests require a valid MINIMAX_API_KEY environment variable.
They make real API calls to the MiniMax API.
"""

import os

import pytest

pytestmark = pytest.mark.skipif(
not os.getenv("MINIMAX_API_KEY"),
reason="MINIMAX_API_KEY not set",
)


@pytest.fixture
def minimax_provider():
"""Create an OpenAICompletionProvider with MiniMax credentials."""
from unittest.mock import patch

with patch.dict(
os.environ,
{"MINIMAX_API_KEY": os.environ.get("MINIMAX_API_KEY", "")},
clear=False,
):
from core.base.providers.llm import CompletionConfig
from core.providers.llm.openai import OpenAICompletionProvider

config = CompletionConfig(provider="openai")
return OpenAICompletionProvider(config)


@pytest.mark.asyncio
async def test_minimax_async_completion(minimax_provider):
"""Test basic async completion with MiniMax M2.7."""
from core.base.abstractions import GenerationConfig

gen_config = GenerationConfig(
model="minimax/MiniMax-M2.7",
temperature=0.1,
max_tokens_to_sample=64,
)
messages = [{"role": "user", "content": "Say hello in one word."}]

response = await minimax_provider.aget_completion(
messages=messages,
generation_config=gen_config,
)
assert response is not None
assert len(response.choices) > 0
assert response.choices[0].message.content is not None
assert len(response.choices[0].message.content) > 0


@pytest.mark.asyncio
async def test_minimax_streaming(minimax_provider):
"""Test streaming completion with MiniMax M2.7."""
from core.base.abstractions import GenerationConfig

gen_config = GenerationConfig(
model="minimax/MiniMax-M2.7",
temperature=0.1,
max_tokens_to_sample=32,
)
messages = [{"role": "user", "content": "Count to 3."}]

chunks = []
async for chunk in minimax_provider.aget_completion_stream(
messages=messages,
generation_config=gen_config,
):
chunks.append(chunk)

assert len(chunks) > 0


@pytest.mark.asyncio
async def test_minimax_json_mode(minimax_provider):
"""Test JSON response format with MiniMax M2.5-highspeed (no thinking tags)."""
import json
import re

from core.base.abstractions import GenerationConfig

gen_config = GenerationConfig(
model="minimax/MiniMax-M2.5-highspeed",
temperature=0.1,
max_tokens_to_sample=512,
response_format={"type": "json_object"},
)
messages = [
{
"role": "user",
"content": 'Return a JSON object with a single key "greeting" and value "hello".',
}
]

response = await minimax_provider.aget_completion(
messages=messages,
generation_config=gen_config,
)
content = response.choices[0].message.content
assert content is not None
# Strip any <think>...</think> tags that some models may include
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
data = json.loads(content)
assert "greeting" in data
Empty file added py/tests/unit/llm/__init__.py
Empty file.
Loading