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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
python tests/test_ci_quick.py
python -m pytest tests/test_plugins.py -v --tb=short || python tests/test_plugins.py
python -m pytest tests/test_proxy_plugin.py -v --tb=short
python -m pytest tests/test_minimax_provider.py -v --tb=short
python tests/test_approaches.py
python tests/test_reasoning_simple.py
python tests/test_batching.py
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ We support all major LLM providers and models for inference. You need to set the
|----------|-------------------------------|------------------|
| OptiLLM | `OPTILLM_API_KEY` | Uses the inbuilt local server for inference, supports logprobs and decoding techniques like `cot_decoding` & `entropy_decoding` |
| OpenAI | `OPENAI_API_KEY` | You can use this with any OpenAI compatible endpoint (e.g. OpenRouter) by setting the `base_url` |
| MiniMax | `MINIMAX_API_KEY` | Uses the [MiniMax API](https://platform.minimax.io/docs/api-reference/text-openai-api) (OpenAI-compatible). Set `MINIMAX_API_REGION=cn` for the China endpoint or `--base-url` for a custom endpoint. Defaults to `MiniMax-M3.1-Flash`; the direct API catalog also lists `MiniMax-M3` (both have 1M context). Explicit `--model`, `OPTILLM_MODEL`, and request model IDs remain unchanged, including legacy IDs. Temperature is passed through unchanged (`[0, 2]` for current models). Flash uses adaptive thinking; `reasoning_effort` accepts `low`, `medium`, `high`, `xhigh`, or `max` (API default). Send `thinking` and `reasoning_split` at the top level or in SDK `extra_body`. Flash rejects disabled thinking, `none` effort, `service_tier`, and `reasoning_split=false`. |
| Cerebras | `CEREBRAS_API_KEY` | You can use this for fast inference with supported models, see [docs for details](https://inference-docs.cerebras.ai/introduction) |
| Azure OpenAI | `AZURE_OPENAI_API_KEY`<br>`AZURE_API_VERSION`<br>`AZURE_API_BASE` | - |
| Azure OpenAI (Managed Identity) | `AZURE_API_VERSION`<br>`AZURE_API_BASE` | Login required using `az login`, see [docs for details](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity) |
Expand Down
72 changes: 72 additions & 0 deletions optillm/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""MiniMax defaults, known-host catalogs, and direct API request options."""
import os
import re
from urllib.parse import urlparse

MINIMAX_MODELS = ("MiniMax-M3.1-Flash", "MiniMax-M3")
OPENROUTER_MINIMAX_MODELS = ("minimax/minimax-m3.1-flash:free", "minimax/minimax-m3")


def minimax_provider_selected():
"""Match get_config's provider precedence without changing explicit models."""
return bool(os.environ.get("MINIMAX_API_KEY")) and not any(
os.environ.get(key) for key in ("OPTILLM_API_KEY", "CEREBRAS_API_KEY")
)


def default_model():
return os.environ.get("OPTILLM_MODEL", MINIMAX_MODELS[0] if minimax_provider_selected() else "gpt-4o-mini")


def is_minimax_client(client):
"""Apply direct API behavior only to verified MiniMax API hosts."""
return urlparse(str(getattr(client, "base_url", ""))).hostname in {
"api.minimax.io", "api.minimaxi.com"
}


def is_minimax_request(client, model):
return is_minimax_client(client) or (
urlparse(str(getattr(client, "base_url", ""))).hostname == 'openrouter.ai'
and model in OPENROUTER_MINIMAX_MODELS
)


def current_minimax_models(client, models):
"""Filter ordinary text choices, keeping provider metadata and nontext rows."""
if is_minimax_client(client):
current = MINIMAX_MODELS
text_pattern = r'^MiniMax-M\d'
elif urlparse(str(getattr(client, "base_url", ""))).hostname == 'openrouter.ai':
current = OPENROUTER_MINIMAX_MODELS
text_pattern = r'^minimax/minimax-(?:m\d|\d)'
else:
return models
# Only reorder/filter returned rows; never synthesize gateway capabilities.
preferred = [row for model in current for row in models if row['id'] == model]
return preferred + [row for row in models if row['id'] not in current and not re.match(text_pattern, row['id'])]


class MiniMaxRequestError(ValueError):
"""A rejected request must not mark a proxy provider unhealthy."""
status_code = 400


def prepare_minimax_request(model, request_config):
"""Forward MiniMax extensions through the SDK's JSON body escape hatch."""
extra_body = dict(request_config.get('extra_body') or {})
for key in ('thinking', 'reasoning_split'):
if key in request_config:
extra_body[key] = request_config.pop(key)
if model == MINIMAX_MODELS[0]:
if (extra_body.get('thinking') or {}).get('type') == 'disabled':
raise MiniMaxRequestError("MiniMax-M3.1-Flash requires adaptive thinking")
effort = extra_body.get('reasoning_effort', request_config.get('reasoning_effort'))
if effort is not None and effort not in ('low', 'medium', 'high', 'xhigh', 'max'):
raise MiniMaxRequestError("MiniMax-M3.1-Flash reasoning_effort must be low, medium, high, xhigh, or max")
if 'service_tier' in request_config or 'service_tier' in extra_body:
raise MiniMaxRequestError("MiniMax-M3.1-Flash does not support service_tier")
if extra_body.get('reasoning_split') is False:
raise MiniMaxRequestError("MiniMax-M3.1-Flash does not support reasoning_split=false")
if extra_body:
request_config['extra_body'] = extra_body
15 changes: 13 additions & 2 deletions optillm/plugins/proxy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import concurrent.futures
import threading
from openai import OpenAI, AzureOpenAI
from optillm.minimax import MINIMAX_MODELS, current_minimax_models, is_minimax_client, prepare_minimax_request
from optillm.plugins.proxy.routing import RouterFactory
from optillm.plugins.proxy.health import HealthChecker

Expand Down Expand Up @@ -110,8 +111,14 @@ def list_provider_models(config: Dict) -> List[Dict[str, Any]]:
for provider_config in config.get('providers', []):
provider = Provider(provider_config)
try:
for model in provider.client.models.list(timeout=config.get('timeouts', {}).get('connect', 5)).data:
model_dict = model.model_dump() if hasattr(model, 'model_dump') else dict(model)
if is_minimax_client(provider.client):
for model_id in MINIMAX_MODELS:
models.setdefault(model_id, {"id": model_id, "object": "model", "created": 0, "owned_by": provider.name})
provider_models = [
model.model_dump() if hasattr(model, 'model_dump') else dict(model)
for model in provider.client.models.list(timeout=config.get('timeouts', {}).get('connect', 5)).data
]
for model_dict in current_minimax_models(provider.client, provider_models):
models.setdefault(model_dict['id'], model_dict)
except Exception as e:
logger.warning(f"Could not list models from provider {provider.name}: {e}")
Expand Down Expand Up @@ -326,6 +333,8 @@ def create(self, **kwargs):
request_kwargs = self._filter_kwargs(kwargs.copy())
mapped_model = provider.map_model(model)
request_kwargs['model'] = mapped_model
if is_minimax_client(provider.client):
prepare_minimax_request(mapped_model, request_kwargs)

# Format messages based on provider's system message support
if 'messages' in request_kwargs:
Expand Down Expand Up @@ -386,6 +395,8 @@ def create(self, **kwargs):
try:
fallback_kwargs = self._filter_kwargs(kwargs.copy())
fallback_kwargs['timeout'] = self.proxy_client.request_timeout
if is_minimax_client(self.proxy_client.fallback_client):
prepare_minimax_request(model, fallback_kwargs)
return self.proxy_client.fallback_client.chat.completions.create(**fallback_kwargs)
except Exception as e:
errors.append(("fallback_client", str(e)))
Expand Down
5 changes: 5 additions & 0 deletions optillm/plugins/proxy_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from optillm.plugins.proxy.config import ProxyConfig
from optillm.plugins.proxy.client import ProxyClient
from optillm.plugins.proxy.approach_handler import ApproachHandler
from optillm.minimax import is_minimax_client, prepare_minimax_request

SLUG = "proxy"
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -68,6 +69,8 @@ def run(system_prompt: str, initial_query: str, client, model: str,
# Strip stream parameter to force complete response
api_config = dict(request_config or {})
api_config.pop('stream', None)
if is_minimax_client(client):
prepare_minimax_request(model, api_config)

response = client.chat.completions.create(
model=model,
Expand Down Expand Up @@ -165,6 +168,8 @@ def run(system_prompt: str, initial_query: str, client, model: str,
# Strip stream parameter to force complete response
api_config = dict(request_config or {})
api_config.pop('stream', None)
if is_minimax_client(client):
prepare_minimax_request(model, api_config)

response = client.chat.completions.create(
model=model,
Expand Down
33 changes: 29 additions & 4 deletions optillm/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
from optillm.mars import multi_agent_reasoning_system
from optillm.batching import RequestBatcher, BatchingError
from optillm.conversation_logger import ConversationLogger
from optillm.minimax import (
MINIMAX_MODELS, current_minimax_models, default_model,
is_minimax_client, is_minimax_request, prepare_minimax_request,
)
import optillm.conversation_logger

# Setup logging
Expand Down Expand Up @@ -92,6 +96,14 @@ def get_config():
default_client = Cerebras(api_key=API_KEY, base_url=base_url, http_client=http_client)
else:
default_client = Cerebras(api_key=API_KEY, http_client=http_client)
elif os.environ.get("MINIMAX_API_KEY"):
API_KEY = os.environ.get("MINIMAX_API_KEY")
base_url = server_config['base_url']
if base_url == "":
region = os.environ.get("MINIMAX_API_REGION", "global").lower()
base_url = "https://api.minimaxi.com/v1" if region == "cn" else "https://api.minimax.io/v1"
default_client = OpenAI(api_key=API_KEY, base_url=base_url, http_client=http_client)
logger.info(f"Created MiniMax client with base_url: {base_url}")
elif os.environ.get("OPENAI_API_KEY"):
API_KEY = os.environ.get("OPENAI_API_KEY")
base_url = server_config['base_url']
Expand Down Expand Up @@ -183,7 +195,7 @@ def count_reasoning_tokens(text: str, tokenizer=None) -> int:
'mcts_exploration': 0.2,
'mcts_depth': 1,
'best_of_n': 3,
'model': 'gpt-4o-mini',
'model': default_model(),
'rstar_max_depth': 3,
'rstar_num_rollouts': 5,
'rstar_c': 1.4,
Expand Down Expand Up @@ -253,8 +265,8 @@ def none_approach(
model = model[5:]

try:
# Normalize message content to ensure it's always string
normalized_messages = normalize_message_content(original_messages)
# Verified MiniMax routes accept structured image/video content parts.
normalized_messages = original_messages if is_minimax_request(client, model) else normalize_message_content(original_messages)

# Prepare request data for logging
provider_request = {
Expand Down Expand Up @@ -905,6 +917,9 @@ def _execute_completion(data: dict, messages: list, bearer_token: str) -> Comple
client = default_client

try:
if 'proxy' not in approaches and is_minimax_client(client):
prepare_minimax_request(model, request_config)

# Route to batch processing if batch mode is enabled
if request_batcher is not None:
try:
Expand Down Expand Up @@ -1193,6 +1208,14 @@ def proxy_models():

if proxy_models_data is not None:
models_data = proxy_models_data
elif is_minimax_client(default_client):
models_data = {
"object": "list",
"data": [
{"id": model, "object": "model", "created": 0, "owned_by": "minimax"}
for model in MINIMAX_MODELS
],
}
elif server_config['base_url']:
client = OpenAI(api_key=API_KEY, base_url=server_config['base_url'])
# For external API, fetch models using the OpenAI client
Expand All @@ -1217,6 +1240,8 @@ def proxy_models():
]
}

if proxy_models_data is None:
models_data['data'] = current_minimax_models(default_client, models_data['data'])
logger.debug('Models retrieved successfully')
return jsonify(models_data), 200
except Exception as e:
Expand Down Expand Up @@ -1246,7 +1271,7 @@ def parse_args():
("--mcts-simulations", "OPTILLM_SIMULATIONS", int, 2, "Number of MCTS simulations"),
("--mcts-exploration", "OPTILLM_EXPLORATION", float, 0.2, "Exploration weight for MCTS"),
("--mcts-depth", "OPTILLM_DEPTH", int, 1, "Simulation depth for MCTS"),
("--model", "OPTILLM_MODEL", str, "gpt-4o-mini", "OpenAI model to use"),
("--model", "OPTILLM_MODEL", str, default_model(), "Model to use"),
("--rstar-max-depth", "OPTILLM_RSTAR_MAX_DEPTH", int, 3, "Maximum depth for rStar algorithm"),
("--rstar-num-rollouts", "OPTILLM_RSTAR_NUM_ROLLOUTS", int, 5, "Number of rollouts for rStar algorithm"),
("--rstar-c", "OPTILLM_RSTAR_C", float, 1.4, "Exploration constant for rStar algorithm"),
Expand Down
87 changes: 87 additions & 0 deletions tests/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Integration tests for MiniMax provider support in optillm.

These tests verify that the MiniMax provider works end-to-end with the actual
MiniMax API. They require a valid MINIMAX_API_KEY environment variable.

Run with: MINIMAX_API_KEY=your-key pytest tests/test_minimax_integration.py -v
"""

import unittest
import os
import sys

# Add parent directory to path to import optillm modules
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Skip all tests if MINIMAX_API_KEY is not set
MINIMAX_API_KEY = os.environ.get('MINIMAX_API_KEY', '')
SKIP_REASON = "MINIMAX_API_KEY environment variable not set"


@unittest.skipUnless(MINIMAX_API_KEY, SKIP_REASON)
class TestMiniMaxIntegration(unittest.TestCase):
"""Integration tests that call the actual MiniMax API."""

def setUp(self):
"""Set up OpenAI client pointing to MiniMax API."""
from openai import OpenAI
self.client = OpenAI(
api_key=MINIMAX_API_KEY,
base_url="https://api.minimax.io/v1"
)

def test_basic_completion(self):
"""Test basic chat completion with MiniMax API."""
response = self.client.chat.completions.create(
model="MiniMax-M3",
messages=[
{"role": "user", "content": "Say hello in one word."}
],
max_tokens=10,
temperature=0.7
)

assert hasattr(response, 'choices')
assert len(response.choices) > 0
assert response.choices[0].message.content is not None
assert len(response.choices[0].message.content) > 0

def test_temperature_boundary(self):
"""Test that MiniMax API accepts temperature at boundary value 0.01."""
response = self.client.chat.completions.create(
model="MiniMax-M3",
messages=[
{"role": "user", "content": "What is 2+2?"}
],
max_tokens=10,
temperature=0.01
)

assert hasattr(response, 'choices')
assert len(response.choices) > 0

def test_streaming_completion(self):
"""Test streaming chat completion with MiniMax API."""
stream = self.client.chat.completions.create(
model="MiniMax-M3",
messages=[
{"role": "user", "content": "Count from 1 to 3."}
],
max_tokens=30,
temperature=0.5,
stream=True
)

chunks = list(stream)
assert len(chunks) > 0
content_chunks = [
chunk.choices[0].delta.content
for chunk in chunks
if chunk.choices[0].delta.content
]
assert len(content_chunks) > 0


if __name__ == '__main__':
unittest.main()
Loading
Loading