From 44b9614814c08f102101dabd6e862c6206308fa3 Mon Sep 17 00:00:00 2001 From: octopus Date: Thu, 26 Mar 2026 12:00:00 +0000 Subject: [PATCH 1/4] feat: add MiniMax as first-class LLM provider Add MiniMax AI (https://www.minimax.io/) as a directly supported LLM provider alongside OpenAI, Cerebras, and Azure OpenAI. MiniMax's API is OpenAI-compatible, so this uses the OpenAI SDK with MiniMax's base URL for seamless integration. Changes: - Add MINIMAX_API_KEY detection in get_config() with auto base URL - Add temperature clamping for MiniMax: values are clamped to (0, 1] - Update README provider table with MiniMax documentation - Add 17 unit tests covering provider detection, priority, and temp clamping - Add 3 integration tests for live API verification --- README.md | 1 + optillm/server.py | 16 ++ tests/test_minimax_integration.py | 87 +++++++++ tests/test_minimax_provider.py | 288 ++++++++++++++++++++++++++++++ 4 files changed, 392 insertions(+) create mode 100644 tests/test_minimax_integration.py create mode 100644 tests/test_minimax_provider.py diff --git a/README.md b/README.md index b3eedf54..ea30c212 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,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://www.minimax.io/) (OpenAI-compatible). Supports MiniMax-M2.7 and other models. Temperature is auto-clamped to (0, 1] | | 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`
`AZURE_API_VERSION`
`AZURE_API_BASE` | - | | Azure OpenAI (Managed Identity) | `AZURE_API_VERSION`
`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) | diff --git a/optillm/server.py b/optillm/server.py index bc2c88f6..05cc5929 100644 --- a/optillm/server.py +++ b/optillm/server.py @@ -91,6 +91,13 @@ 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 == "": + base_url = "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'] @@ -759,6 +766,15 @@ def proxy(): base_url = server_config['base_url'] default_client, api_key = get_config() + # Clamp temperature for MiniMax provider: must be in (0.0, 1.0] + if os.environ.get("MINIMAX_API_KEY") and 'temperature' in request_config: + temp = request_config['temperature'] + if temp is not None: + if temp <= 0: + request_config['temperature'] = 0.01 + elif temp > 1.0: + request_config['temperature'] = 1.0 + operation, approaches, model = parse_combined_approach(model, known_approaches, plugin_approaches) # Start conversation logging if enabled diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py new file mode 100644 index 00000000..071549dc --- /dev/null +++ b/tests/test_minimax_integration.py @@ -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-M2.7", + 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-M2.7", + 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-M2.7", + 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() diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py new file mode 100644 index 00000000..017d91b4 --- /dev/null +++ b/tests/test_minimax_provider.py @@ -0,0 +1,288 @@ +""" +Unit tests for MiniMax provider support in optillm. + +Tests verify that the MiniMax provider: +- Is correctly detected via MINIMAX_API_KEY environment variable +- Creates an OpenAI client with the MiniMax base URL +- Respects custom base_url when set +- Takes priority over OpenAI when both keys are set +- Does not interfere when MINIMAX_API_KEY is not set +- Properly clamps temperature to MiniMax's valid range (0, 1] +""" + +import unittest +from unittest.mock import patch, MagicMock +import sys +import os +import json + +# Add parent directory to path to import optillm modules +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Mock heavy dependencies that may not be installed in test environments +for mod_name in ['z3', 'torch', 'torch.nn', 'torch.nn.functional', + 'transformers', 'adaptive_classifier', + 'peft', 'bitsandbytes', 'outlines', 'spacy', + 'presidio_analyzer', 'presidio_anonymizer']: + if mod_name not in sys.modules: + sys.modules[mod_name] = MagicMock() + +from optillm import server_config + + +class TestMiniMaxProviderDetection(unittest.TestCase): + """Test MiniMax provider detection via MINIMAX_API_KEY.""" + + def setUp(self): + """Reset server_config before each test.""" + self.original_config = server_config.copy() + # Clear provider-related environment variables + for key in ['MINIMAX_API_KEY', 'OPENAI_API_KEY', 'CEREBRAS_API_KEY', + 'AZURE_OPENAI_API_KEY', 'OPTILLM_API_KEY']: + if key in os.environ: + del os.environ[key] + + def tearDown(self): + """Restore original server_config after each test.""" + server_config.clear() + server_config.update(self.original_config) + + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) + def test_minimax_provider_detected(self): + """Test that MINIMAX_API_KEY triggers MiniMax provider.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + client, api_key = get_config() + + # Should use the MiniMax API key + assert api_key == 'test-minimax-key' + + # Should create OpenAI client with MiniMax base URL + mock_openai.assert_called_once() + call_kwargs = mock_openai.call_args[1] + assert call_kwargs['api_key'] == 'test-minimax-key' + assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' + + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) + def test_minimax_default_base_url(self): + """Test MiniMax provider uses default base URL https://api.minimax.io/v1.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + get_config() + call_kwargs = mock_openai.call_args[1] + assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' + + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) + def test_minimax_with_custom_base_url(self): + """Test MiniMax provider with custom base_url.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = 'https://custom-proxy.example.com/v1' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + client, api_key = get_config() + + call_kwargs = mock_openai.call_args[1] + assert call_kwargs['base_url'] == 'https://custom-proxy.example.com/v1' + + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) + def test_minimax_client_receives_http_client(self): + """Test that MiniMax client receives the configured httpx client.""" + from optillm.server import get_config + + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + mock_http_client_instance = MagicMock() + + with patch('httpx.Client', return_value=mock_http_client_instance) as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + get_config() + + call_kwargs = mock_openai.call_args[1] + assert 'http_client' in call_kwargs + assert call_kwargs['http_client'] == mock_http_client_instance + + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'minimax-key', 'OPENAI_API_KEY': 'openai-key'}) + def test_minimax_takes_priority_over_openai(self): + """Test that MINIMAX_API_KEY takes priority over OPENAI_API_KEY.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + client, api_key = get_config() + + assert api_key == 'minimax-key' + call_kwargs = mock_openai.call_args[1] + assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'openai-key'}) + def test_openai_still_works_without_minimax(self): + """Test that OpenAI provider works when MINIMAX_API_KEY is not set.""" + from optillm.server import get_config + + # Ensure MINIMAX_API_KEY is not set + if 'MINIMAX_API_KEY' in os.environ: + del os.environ['MINIMAX_API_KEY'] + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + client, api_key = get_config() + + assert api_key == 'openai-key' + call_kwargs = mock_openai.call_args[1] + # Should NOT have MiniMax base URL + assert 'base_url' not in call_kwargs + + @patch.dict(os.environ, {'CEREBRAS_API_KEY': 'cerebras-key', 'MINIMAX_API_KEY': 'minimax-key'}) + def test_cerebras_takes_priority_over_minimax(self): + """Test that CEREBRAS_API_KEY takes priority over MINIMAX_API_KEY.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.Cerebras') as mock_cerebras: + client, api_key = get_config() + + assert api_key == 'cerebras-key' + mock_cerebras.assert_called_once() + + +class TestMiniMaxTemperatureClamping(unittest.TestCase): + """Test temperature clamping for MiniMax provider. + + The clamping logic in server.py's proxy() checks MINIMAX_API_KEY env var + and adjusts temperature in request_config to MiniMax's valid range (0, 1]. + These tests verify the clamping logic directly. + """ + + def _clamp_temperature(self, temp, is_minimax=True): + """Simulate the temperature clamping logic from server.py proxy().""" + request_config = {'temperature': temp} + if is_minimax and 'temperature' in request_config: + t = request_config['temperature'] + if t is not None: + if t <= 0: + request_config['temperature'] = 0.01 + elif t > 1.0: + request_config['temperature'] = 1.0 + return request_config['temperature'] + + def test_temperature_zero_clamped(self): + """Test that temperature=0 is clamped to 0.01 for MiniMax.""" + assert self._clamp_temperature(0) == 0.01 + + def test_temperature_negative_clamped(self): + """Test that negative temperature is clamped to 0.01 for MiniMax.""" + assert self._clamp_temperature(-0.5) == 0.01 + + def test_temperature_above_one_clamped(self): + """Test that temperature > 1.0 is clamped to 1.0 for MiniMax.""" + assert self._clamp_temperature(1.5) == 1.0 + + def test_temperature_exactly_two_clamped(self): + """Test that temperature=2.0 is clamped to 1.0 for MiniMax.""" + assert self._clamp_temperature(2.0) == 1.0 + + def test_valid_temperature_point_seven_unchanged(self): + """Test that valid temperature 0.7 is not modified.""" + assert self._clamp_temperature(0.7) == 0.7 + + def test_valid_temperature_one_unchanged(self): + """Test that temperature=1.0 (boundary) is not modified.""" + assert self._clamp_temperature(1.0) == 1.0 + + def test_valid_temperature_point_one_unchanged(self): + """Test that temperature=0.1 is not modified.""" + assert self._clamp_temperature(0.1) == 0.1 + + def test_no_clamping_for_non_minimax(self): + """Test that temperature is NOT clamped when not using MiniMax.""" + assert self._clamp_temperature(0, is_minimax=False) == 0 + assert self._clamp_temperature(1.5, is_minimax=False) == 1.5 + assert self._clamp_temperature(-1, is_minimax=False) == -1 + + +class TestMiniMaxProviderPriority(unittest.TestCase): + """Test provider priority ordering with MiniMax.""" + + def setUp(self): + """Set up test environment.""" + self.original_config = server_config.copy() + for key in ['MINIMAX_API_KEY', 'OPENAI_API_KEY', 'CEREBRAS_API_KEY', + 'AZURE_OPENAI_API_KEY', 'OPTILLM_API_KEY']: + if key in os.environ: + del os.environ[key] + + def tearDown(self): + """Restore original server_config.""" + server_config.clear() + server_config.update(self.original_config) + + @patch.dict(os.environ, {'OPTILLM_API_KEY': 'optillm-key', 'MINIMAX_API_KEY': 'minimax-key'}) + def test_optillm_takes_priority_over_minimax(self): + """Test that OPTILLM_API_KEY takes priority over MINIMAX_API_KEY.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + # Mock the local inference path + with patch.dict(sys.modules, {'optillm.inference': MagicMock()}): + # Reimport to pick up the mock + import importlib + from optillm import server + importlib.reload(server) + client, api_key = server.get_config() + assert api_key == 'optillm-key' + + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'minimax-key', 'AZURE_OPENAI_API_KEY': 'azure-key', + 'AZURE_API_VERSION': '2024-02-15', 'AZURE_API_BASE': 'https://test.openai.azure.com'}) + def test_minimax_takes_priority_over_azure(self): + """Test that MINIMAX_API_KEY takes priority over AZURE_OPENAI_API_KEY.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + client, api_key = get_config() + assert api_key == 'minimax-key' + call_kwargs = mock_openai.call_args[1] + assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' + + +if __name__ == '__main__': + unittest.main() From 71fb88c2922279fd1121e65594c66b6093f1b241 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sun, 7 Jun 2026 14:35:13 +0800 Subject: [PATCH 2/4] feat: upgrade MiniMax default model to M3 - Update README to list MiniMax-M3 as default (512K context, max output 128K) - Keep MiniMax-M2.7 and MiniMax-M2.7-highspeed as alternatives - Update integration tests to use MiniMax-M3 --- README.md | 2 +- tests/test_minimax_integration.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ea30c212..2237c697 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,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://www.minimax.io/) (OpenAI-compatible). Supports MiniMax-M2.7 and other models. Temperature is auto-clamped to (0, 1] | +| MiniMax | `MINIMAX_API_KEY` | Uses the [MiniMax API](https://www.minimax.io/) (OpenAI-compatible). Supports MiniMax-M3 (default, 512K context, max output 128K), MiniMax-M2.7, and MiniMax-M2.7-highspeed. Temperature is auto-clamped to (0, 1] | | 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`
`AZURE_API_VERSION`
`AZURE_API_BASE` | - | | Azure OpenAI (Managed Identity) | `AZURE_API_VERSION`
`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) | diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py index 071549dc..0fa4ba6f 100644 --- a/tests/test_minimax_integration.py +++ b/tests/test_minimax_integration.py @@ -34,7 +34,7 @@ def setUp(self): def test_basic_completion(self): """Test basic chat completion with MiniMax API.""" response = self.client.chat.completions.create( - model="MiniMax-M2.7", + model="MiniMax-M3", messages=[ {"role": "user", "content": "Say hello in one word."} ], @@ -50,7 +50,7 @@ def test_basic_completion(self): def test_temperature_boundary(self): """Test that MiniMax API accepts temperature at boundary value 0.01.""" response = self.client.chat.completions.create( - model="MiniMax-M2.7", + model="MiniMax-M3", messages=[ {"role": "user", "content": "What is 2+2?"} ], @@ -64,7 +64,7 @@ def test_temperature_boundary(self): def test_streaming_completion(self): """Test streaming chat completion with MiniMax API.""" stream = self.client.chat.completions.create( - model="MiniMax-M2.7", + model="MiniMax-M3", messages=[ {"role": "user", "content": "Count from 1 to 3."} ], From 688e60618b79f11eba03fc98f18586335fbc20a9 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:10:39 +0800 Subject: [PATCH 3/4] fix: refresh MiniMax endpoint metadata --- README.md | 2 +- optillm/server.py | 3 ++- tests/test_minimax_provider.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2237c697..d10603e1 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,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://www.minimax.io/) (OpenAI-compatible). Supports MiniMax-M3 (default, 512K context, max output 128K), MiniMax-M2.7, and MiniMax-M2.7-highspeed. Temperature is auto-clamped to (0, 1] | +| MiniMax | `MINIMAX_API_KEY` | Uses the [MiniMax API](https://www.minimax.io/) (OpenAI-compatible). Set `MINIMAX_API_REGION=cn` for the China endpoint. Supports MiniMax-M3 (default, 1M context, max output 128K), MiniMax-M2.7, and MiniMax-M2.7-highspeed. Temperature is auto-clamped to (0, 1] | | 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`
`AZURE_API_VERSION`
`AZURE_API_BASE` | - | | Azure OpenAI (Managed Identity) | `AZURE_API_VERSION`
`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) | diff --git a/optillm/server.py b/optillm/server.py index 05cc5929..fc6bf5db 100644 --- a/optillm/server.py +++ b/optillm/server.py @@ -95,7 +95,8 @@ def get_config(): API_KEY = os.environ.get("MINIMAX_API_KEY") base_url = server_config['base_url'] if base_url == "": - base_url = "https://api.minimax.io/v1" + 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"): diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 017d91b4..963195be 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -84,6 +84,19 @@ def test_minimax_default_base_url(self): call_kwargs = mock_openai.call_args[1] assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key', 'MINIMAX_API_REGION': 'cn'}) + def test_minimax_china_base_url(self): + """Test MiniMax provider selects the China endpoint explicitly.""" + from optillm.server import get_config + + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + with patch('httpx.Client'), patch('optillm.server.OpenAI') as mock_openai: + get_config() + assert mock_openai.call_args[1]['base_url'] == 'https://api.minimaxi.com/v1' + @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) def test_minimax_with_custom_base_url(self): """Test MiniMax provider with custom base_url.""" From 2a3c4d2140fc30c02439ad4e0beed45987cd68c8 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:32:57 +0800 Subject: [PATCH 4/4] feat: refresh MiniMax defaults and transport for M3.1 Flash --- .github/workflows/test.yml | 1 + README.md | 2 +- optillm/minimax.py | 72 +++++ optillm/plugins/proxy/client.py | 15 +- optillm/plugins/proxy_plugin.py | 5 + optillm/server.py | 34 +- tests/test_minimax_provider.py | 545 +++++++++++++++----------------- 7 files changed, 367 insertions(+), 307 deletions(-) create mode 100644 optillm/minimax.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1173b0ef..81860cb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/README.md b/README.md index 9a7a388a..c2f24b97 100644 --- a/README.md +++ b/README.md @@ -224,7 +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://www.minimax.io/) (OpenAI-compatible). Set `MINIMAX_API_REGION=cn` for the China endpoint. Supports MiniMax-M3 (default, 1M context, max output 128K), MiniMax-M2.7, and MiniMax-M2.7-highspeed. Temperature is auto-clamped to (0, 1] | +| 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`
`AZURE_API_VERSION`
`AZURE_API_BASE` | - | | Azure OpenAI (Managed Identity) | `AZURE_API_VERSION`
`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) | diff --git a/optillm/minimax.py b/optillm/minimax.py new file mode 100644 index 00000000..b7e99454 --- /dev/null +++ b/optillm/minimax.py @@ -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 diff --git a/optillm/plugins/proxy/client.py b/optillm/plugins/proxy/client.py index 5fbc0f5e..eb8d6e08 100644 --- a/optillm/plugins/proxy/client.py +++ b/optillm/plugins/proxy/client.py @@ -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 @@ -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}") @@ -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: @@ -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))) diff --git a/optillm/plugins/proxy_plugin.py b/optillm/plugins/proxy_plugin.py index 151b0421..47ed08d2 100644 --- a/optillm/plugins/proxy_plugin.py +++ b/optillm/plugins/proxy_plugin.py @@ -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__) @@ -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, @@ -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, diff --git a/optillm/server.py b/optillm/server.py index d0979e7b..c9878bb2 100644 --- a/optillm/server.py +++ b/optillm/server.py @@ -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 @@ -191,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, @@ -261,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 = { @@ -880,15 +884,6 @@ def _execute_completion(data: dict, messages: list, bearer_token: str) -> Comple base_url = server_config['base_url'] default_client, api_key = get_config() - # Clamp temperature for MiniMax provider: must be in (0.0, 1.0] - if os.environ.get("MINIMAX_API_KEY") and 'temperature' in request_config: - temp = request_config['temperature'] - if temp is not None: - if temp <= 0: - request_config['temperature'] = 0.01 - elif temp > 1.0: - request_config['temperature'] = 1.0 - operation, approaches, model = parse_combined_approach(model, known_approaches, plugin_approaches) # Start conversation logging if enabled @@ -922,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: @@ -1210,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 @@ -1234,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: @@ -1263,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"), diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 963195be..40ac0542 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -1,300 +1,263 @@ -""" -Unit tests for MiniMax provider support in optillm. - -Tests verify that the MiniMax provider: -- Is correctly detected via MINIMAX_API_KEY environment variable -- Creates an OpenAI client with the MiniMax base URL -- Respects custom base_url when set -- Takes priority over OpenAI when both keys are set -- Does not interfere when MINIMAX_API_KEY is not set -- Properly clamps temperature to MiniMax's valid range (0, 1] -""" - -import unittest -from unittest.mock import patch, MagicMock -import sys -import os +"""Offline provider tests through Flask and the real SDK's HTTP serialization.""" import json +import os +import sys +import subprocess +import unittest +from unittest.mock import MagicMock, patch -# Add parent directory to path to import optillm modules -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -# Mock heavy dependencies that may not be installed in test environments -for mod_name in ['z3', 'torch', 'torch.nn', 'torch.nn.functional', - 'transformers', 'adaptive_classifier', - 'peft', 'bitsandbytes', 'outlines', 'spacy', - 'presidio_analyzer', 'presidio_anonymizer']: - if mod_name not in sys.modules: - sys.modules[mod_name] = MagicMock() - -from optillm import server_config - - -class TestMiniMaxProviderDetection(unittest.TestCase): - """Test MiniMax provider detection via MINIMAX_API_KEY.""" - - def setUp(self): - """Reset server_config before each test.""" - self.original_config = server_config.copy() - # Clear provider-related environment variables - for key in ['MINIMAX_API_KEY', 'OPENAI_API_KEY', 'CEREBRAS_API_KEY', - 'AZURE_OPENAI_API_KEY', 'OPTILLM_API_KEY']: - if key in os.environ: - del os.environ[key] - - def tearDown(self): - """Restore original server_config after each test.""" - server_config.clear() - server_config.update(self.original_config) - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) - def test_minimax_provider_detected(self): - """Test that MINIMAX_API_KEY triggers MiniMax provider.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - client, api_key = get_config() - - # Should use the MiniMax API key - assert api_key == 'test-minimax-key' - - # Should create OpenAI client with MiniMax base URL - mock_openai.assert_called_once() - call_kwargs = mock_openai.call_args[1] - assert call_kwargs['api_key'] == 'test-minimax-key' - assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) - def test_minimax_default_base_url(self): - """Test MiniMax provider uses default base URL https://api.minimax.io/v1.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - get_config() - call_kwargs = mock_openai.call_args[1] - assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key', 'MINIMAX_API_REGION': 'cn'}) - def test_minimax_china_base_url(self): - """Test MiniMax provider selects the China endpoint explicitly.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client'), patch('optillm.server.OpenAI') as mock_openai: - get_config() - assert mock_openai.call_args[1]['base_url'] == 'https://api.minimaxi.com/v1' - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) - def test_minimax_with_custom_base_url(self): - """Test MiniMax provider with custom base_url.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = 'https://custom-proxy.example.com/v1' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - client, api_key = get_config() - - call_kwargs = mock_openai.call_args[1] - assert call_kwargs['base_url'] == 'https://custom-proxy.example.com/v1' - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-minimax-key'}) - def test_minimax_client_receives_http_client(self): - """Test that MiniMax client receives the configured httpx client.""" - from optillm.server import get_config - - server_config['ssl_verify'] = False - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - mock_http_client_instance = MagicMock() - - with patch('httpx.Client', return_value=mock_http_client_instance) as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - get_config() - - call_kwargs = mock_openai.call_args[1] - assert 'http_client' in call_kwargs - assert call_kwargs['http_client'] == mock_http_client_instance - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'minimax-key', 'OPENAI_API_KEY': 'openai-key'}) - def test_minimax_takes_priority_over_openai(self): - """Test that MINIMAX_API_KEY takes priority over OPENAI_API_KEY.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - client, api_key = get_config() - - assert api_key == 'minimax-key' - call_kwargs = mock_openai.call_args[1] - assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' - - @patch.dict(os.environ, {'OPENAI_API_KEY': 'openai-key'}) - def test_openai_still_works_without_minimax(self): - """Test that OpenAI provider works when MINIMAX_API_KEY is not set.""" - from optillm.server import get_config - - # Ensure MINIMAX_API_KEY is not set - if 'MINIMAX_API_KEY' in os.environ: - del os.environ['MINIMAX_API_KEY'] - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - client, api_key = get_config() - - assert api_key == 'openai-key' - call_kwargs = mock_openai.call_args[1] - # Should NOT have MiniMax base URL - assert 'base_url' not in call_kwargs - - @patch.dict(os.environ, {'CEREBRAS_API_KEY': 'cerebras-key', 'MINIMAX_API_KEY': 'minimax-key'}) - def test_cerebras_takes_priority_over_minimax(self): - """Test that CEREBRAS_API_KEY takes priority over MINIMAX_API_KEY.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.Cerebras') as mock_cerebras: - client, api_key = get_config() - - assert api_key == 'cerebras-key' - mock_cerebras.assert_called_once() - - -class TestMiniMaxTemperatureClamping(unittest.TestCase): - """Test temperature clamping for MiniMax provider. - - The clamping logic in server.py's proxy() checks MINIMAX_API_KEY env var - and adjusts temperature in request_config to MiniMax's valid range (0, 1]. - These tests verify the clamping logic directly. - """ - - def _clamp_temperature(self, temp, is_minimax=True): - """Simulate the temperature clamping logic from server.py proxy().""" - request_config = {'temperature': temp} - if is_minimax and 'temperature' in request_config: - t = request_config['temperature'] - if t is not None: - if t <= 0: - request_config['temperature'] = 0.01 - elif t > 1.0: - request_config['temperature'] = 1.0 - return request_config['temperature'] - - def test_temperature_zero_clamped(self): - """Test that temperature=0 is clamped to 0.01 for MiniMax.""" - assert self._clamp_temperature(0) == 0.01 - - def test_temperature_negative_clamped(self): - """Test that negative temperature is clamped to 0.01 for MiniMax.""" - assert self._clamp_temperature(-0.5) == 0.01 - - def test_temperature_above_one_clamped(self): - """Test that temperature > 1.0 is clamped to 1.0 for MiniMax.""" - assert self._clamp_temperature(1.5) == 1.0 - - def test_temperature_exactly_two_clamped(self): - """Test that temperature=2.0 is clamped to 1.0 for MiniMax.""" - assert self._clamp_temperature(2.0) == 1.0 - - def test_valid_temperature_point_seven_unchanged(self): - """Test that valid temperature 0.7 is not modified.""" - assert self._clamp_temperature(0.7) == 0.7 - - def test_valid_temperature_one_unchanged(self): - """Test that temperature=1.0 (boundary) is not modified.""" - assert self._clamp_temperature(1.0) == 1.0 - - def test_valid_temperature_point_one_unchanged(self): - """Test that temperature=0.1 is not modified.""" - assert self._clamp_temperature(0.1) == 0.1 - - def test_no_clamping_for_non_minimax(self): - """Test that temperature is NOT clamped when not using MiniMax.""" - assert self._clamp_temperature(0, is_minimax=False) == 0 - assert self._clamp_temperature(1.5, is_minimax=False) == 1.5 - assert self._clamp_temperature(-1, is_minimax=False) == -1 +import httpx +from openai import OpenAI +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from optillm import server +from optillm.plugins.proxy.client import ProxyClient, list_provider_models -class TestMiniMaxProviderPriority(unittest.TestCase): - """Test provider priority ordering with MiniMax.""" +class TestMiniMaxProvider(unittest.TestCase): def setUp(self): - """Set up test environment.""" - self.original_config = server_config.copy() - for key in ['MINIMAX_API_KEY', 'OPENAI_API_KEY', 'CEREBRAS_API_KEY', - 'AZURE_OPENAI_API_KEY', 'OPTILLM_API_KEY']: - if key in os.environ: - del os.environ[key] - - def tearDown(self): - """Restore original server_config.""" - server_config.clear() - server_config.update(self.original_config) - - @patch.dict(os.environ, {'OPTILLM_API_KEY': 'optillm-key', 'MINIMAX_API_KEY': 'minimax-key'}) - def test_optillm_takes_priority_over_minimax(self): - """Test that OPTILLM_API_KEY takes priority over MINIMAX_API_KEY.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - # Mock the local inference path - with patch.dict(sys.modules, {'optillm.inference': MagicMock()}): - # Reimport to pick up the mock - import importlib - from optillm import server - importlib.reload(server) - client, api_key = server.get_config() - assert api_key == 'optillm-key' - - @patch.dict(os.environ, {'MINIMAX_API_KEY': 'minimax-key', 'AZURE_OPENAI_API_KEY': 'azure-key', - 'AZURE_API_VERSION': '2024-02-15', 'AZURE_API_BASE': 'https://test.openai.azure.com'}) - def test_minimax_takes_priority_over_azure(self): - """Test that MINIMAX_API_KEY takes priority over AZURE_OPENAI_API_KEY.""" - from optillm.server import get_config - - server_config['ssl_verify'] = True - server_config['ssl_cert_path'] = '' - server_config['base_url'] = '' - - with patch('httpx.Client') as mock_httpx_client, \ - patch('optillm.server.OpenAI') as mock_openai: - client, api_key = get_config() - assert api_key == 'minimax-key' - call_kwargs = mock_openai.call_args[1] - assert call_kwargs['base_url'] == 'https://api.minimax.io/v1' + self.env = patch.dict(os.environ, {'MINIMAX_API_KEY': 'fixture-key'}, clear=True) + self.env.start() + self.addCleanup(self.env.stop) + self.config = patch.dict(server.server_config, { + 'base_url': '', 'optillm_api_key': '', 'approach': 'none', + 'model': server.default_model(), 'ssl_verify': True, 'ssl_cert_path': '', + }) + self.config.start() + self.addCleanup(self.config.stop) + self.requests = [] + self.transport = httpx.MockTransport(self.respond) + self.http = httpx.Client(transport=self.transport) + self.addCleanup(self.http.close) + self.http_patch = patch('httpx.HTTPTransport.handle_request', side_effect=self.respond) + self.http_patch.start() + self.addCleanup(self.http_patch.stop) + self.models_patch = patch('optillm.plugins.proxy.client.list_provider_models', return_value=None) + self.models_patch.start() + self.addCleanup(self.models_patch.stop) + self.web = server.app.test_client() + + def respond(self, request): + self.requests.append(request) + if request.method == 'GET': + models = ['MiniMax-M2.7', 'MiniMax-M3', 'speech-2.8-hd', 'custom-model'] + if request.url.host == 'openrouter.ai': + models = ['minimax/minimax-m2.7', 'minimax/minimax-m3', + 'minimax/minimax-m3.1-flash:free', 'other/model'] + return httpx.Response(200, json={'object': 'list', 'data': [ + {'id': model, 'object': 'model', 'created': 0, 'owned_by': 'fixture', + 'context_length': 12345, 'pricing': {'prompt': 'fixture-price'}} + for model in models + ]}) + body = json.loads(request.content) + return httpx.Response(200, json={ + 'id': 'chatcmpl-fixture', 'object': 'chat.completion', 'created': 0, + 'model': body['model'], + 'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'ok'}, 'finish_reason': 'stop'}], + 'usage': {'prompt_tokens': 1, 'completion_tokens': 1, 'total_tokens': 2}, + }) + + def post(self, payload=None, path='/v1/chat/completions', headers=None): + data = {'messages': [{'role': 'user', 'content': 'Hello'}]} + data.update(payload or {}) + response = self.web.post(path, json=data, headers=headers or {}) + self.assertEqual(response.status_code, 200, response.get_data(as_text=True)) + return json.loads(self.requests[-1].content) + + def test_startup_default_without_model_override(self): + code = """ +import json +from unittest.mock import patch +import httpx +from optillm import server +requests = [] +def respond(request): + requests.append(json.loads(request.content)) + return httpx.Response(200, json={'id': 'fixture', 'model': requests[-1]['model'], + 'object': 'chat.completion', 'created': 0, 'choices': [ + {'index': 0, 'message': {'role': 'assistant', 'content': 'ok'}, 'finish_reason': 'stop'}], + 'usage': {'prompt_tokens': 1, 'completion_tokens': 1, 'total_tokens': 2}}) +with patch('httpx.HTTPTransport.handle_request', side_effect=respond): + response = server.app.test_client().post('/v1/chat/completions', + json={'messages': [{'role': 'user', 'content': 'Hello'}]}) +assert response.status_code == 200, response.get_data(as_text=True) +assert requests[-1]['model'] == 'MiniMax-M3.1-Flash', requests +""" + result = subprocess.run([sys.executable, '-c', code], env={'MINIMAX_API_KEY': 'fixture-key'}, + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_default_regions_and_catalog(self): + for region, host in [('global', 'api.minimax.io'), ('cn', 'api.minimaxi.com')]: + with self.subTest(region=region), patch.dict(os.environ, {'MINIMAX_API_REGION': region}): + body = self.post() + self.assertEqual(body['model'], 'MiniMax-M3.1-Flash') + self.assertEqual(self.requests[-1].url.host, host) + self.assertEqual(self.requests[-1].url.path, '/v1/chat/completions') + self.assertEqual(self.requests[-1].headers['authorization'], 'Bearer fixture-key') + for key in ('thinking', 'reasoning_effort', 'service_tier', 'reasoning_split'): + self.assertNotIn(key, body) + models = self.web.get('/v1/models').get_json()['data'] + self.assertEqual([m['id'] for m in models], list(server.MINIMAX_MODELS)) + + def test_cli_and_saved_model_precedence(self): + for argv, env, expected in [ + (['optillm'], {}, 'MiniMax-M3.1-Flash'), + (['optillm'], {'OPTILLM_MODEL': 'MiniMax-M2.7'}, 'MiniMax-M2.7'), + (['optillm', '--model', 'custom-model'], {'OPTILLM_MODEL': 'MiniMax-M3'}, 'custom-model'), + ]: + with self.subTest(expected=expected), patch.dict(os.environ, env), patch.object(sys, 'argv', argv): + args = server.parse_args() + server.server_config['model'] = args.model + self.assertEqual(self.post()['model'], expected) + + def test_explicit_models_and_temperature_are_preserved(self): + for model in ('MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'custom-model', 'none-MiniMax-M3.1-Flash'): + for temperature in (0, 1.5, 2): + with self.subTest(model=model, temperature=temperature): + body = self.post({'model': model, 'temperature': temperature}) + self.assertEqual(body['model'], model.removeprefix('none-')) + self.assertEqual(body['temperature'], temperature) + + def test_custom_endpoint_is_not_given_direct_api_rules(self): + server.server_config['base_url'] = 'https://custom.example/v1' + body = self.post({'model': 'custom-model', 'temperature': 0, 'service_tier': 'priority', + 'extra_body': {'thinking': {'type': 'disabled'}}}) + self.assertEqual(self.requests[-1].url.host, 'custom.example') + self.assertEqual(body['thinking'], {'type': 'disabled'}) + self.assertEqual(body['temperature'], 0) + self.assertEqual(body['model'], 'custom-model') + models = self.web.get('/v1/models').get_json()['data'] + self.assertIn('MiniMax-M2.7', [m['id'] for m in models]) + + def test_openrouter_catalog_and_saved_request_keep_gateway_contract(self): + server.server_config['base_url'] = 'https://openrouter.ai/api/v1' + models = self.web.get('/v1/models').get_json()['data'] + self.assertEqual([m['id'] for m in models], [ + 'minimax/minimax-m3.1-flash:free', 'minimax/minimax-m3', 'other/model']) + self.assertEqual(models[0]['context_length'], 12345) + self.assertEqual(models[0]['pricing'], {'prompt': 'fixture-price'}) + for model in ('minimax/minimax-m2.7', 'minimax/minimax-m3.1-flash:free'): + body = self.post({'model': model, 'extra_body': {'reasoning': {'effort': 'high'}}}) + self.assertEqual(body['model'], model) + self.assertEqual(body['reasoning'], {'effort': 'high'}) + self.assertNotIn('thinking', body) + self.assertEqual(self.requests[-1].url.path, '/api/v1/chat/completions') + + def test_effort_thinking_and_token_limit_serialization(self): + for effort in ('low', 'medium', 'high', 'xhigh', 'max'): + with self.subTest(effort=effort): + body = self.post({'reasoning_effort': effort, 'thinking': {'type': 'adaptive'}, + 'reasoning_split': True, 'max_completion_tokens': 524288}) + self.assertEqual(body['reasoning_effort'], effort) + self.assertEqual(body['thinking'], {'type': 'adaptive'}) + self.assertTrue(body['reasoning_split']) + self.assertEqual(body['max_completion_tokens'], 524288) + self.assertNotIn('extra_body', body) + body = self.post({'model': 'MiniMax-M3', 'thinking': {'type': 'disabled'}, + 'reasoning_effort': 'high', 'service_tier': 'priority', 'reasoning_split': False}) + self.assertEqual(body['thinking'], {'type': 'disabled'}) + self.assertEqual(body['service_tier'], 'priority') + self.assertFalse(body['reasoning_split']) + + def test_flash_rejects_unsupported_options_without_sending(self): + for options in ({'thinking': {'type': 'disabled'}}, {'reasoning_effort': 'none'}, + {'service_tier': 'standard'}, {'reasoning_split': False}, + {'extra_body': {'thinking': {'type': 'disabled'}}}, + {'extra_body': {'service_tier': 'priority'}}, + {'extra_body': {'reasoning_effort': 'none'}}): + with self.subTest(options=options): + self.requests.clear() + response = self.web.post('/v1/chat/completions', json={ + 'messages': [{'role': 'user', 'content': 'Hello'}], **options}) + self.assertEqual(response.status_code, 500) + self.assertIn('MiniMax-M3.1-Flash', response.get_json()['error']) + self.assertEqual(self.requests, []) + + def test_multimodal_and_tool_history_survive_transport(self): + messages = [ + {'role': 'user', 'content': [ + {'type': 'text', 'text': 'Describe'}, + {'type': 'image_url', 'image_url': {'url': 'https://example.test/image.png'}}, + {'type': 'video_url', 'video_url': {'url': 'mm_file://fixture'}}, + ]}, + {'role': 'assistant', 'content': 'keep', 'reasoning_content': 'keep too', + 'tool_calls': [{'id': 'call_fixture', 'type': 'function', 'function': {'name': 'lookup', 'arguments': '{}'}}]}, + {'role': 'tool', 'tool_call_id': 'call_fixture', 'content': 'found'}, + ] + body = self.post({'messages': messages}) + self.assertEqual(body['messages'], messages) + server.server_config['base_url'] = 'https://openrouter.ai/api/v1' + body = self.post({'model': 'minimax/minimax-m3.1-flash:free', 'messages': messages}) + self.assertEqual(body['messages'], messages) + + def test_legacy_completion_and_streaming_routes(self): + body = self.post({'prompt': 'Hello'}, path='/v1/completions') + self.assertEqual(body['model'], 'MiniMax-M3.1-Flash') + response = self.web.post('/v1/chat/completions', json={ + 'messages': [{'role': 'user', 'content': 'Hello'}], 'stream': True}) + self.assertEqual(response.status_code, 200) + self.assertIn(b'[DONE]', response.data) + self.assertNotIn('stream', json.loads(self.requests[-1].content)) + + def test_higher_priority_provider_and_bearer_override(self): + with patch.dict(os.environ, {'CEREBRAS_API_KEY': 'fixture-other'}): + with patch.object(server, 'Cerebras', return_value=OpenAI( + api_key='fixture-other', base_url='https://other.example/v1', http_client=self.http)): + self.assertEqual(server.default_model(), 'gpt-4o-mini') + body = self.post({'model': 'other-model', 'temperature': 0}) + self.assertEqual(body['temperature'], 0) + self.assertEqual(self.requests[-1].url.host, 'other.example') + with patch.dict(os.environ, {'OPTILLM_API_KEY': 'fixture-local'}), patch.dict( + sys.modules, {'optillm.inference': MagicMock()}): + self.assertEqual(server.default_model(), 'gpt-4o-mini') + self.assertEqual(server.get_config()[1], 'fixture-local') + self.post({'model': 'other-model', 'temperature': 0}, headers={'Authorization': 'Bearer sk-fixture'}) + self.assertEqual(self.requests[-1].url.host, 'api.openai.com') + + def test_lower_priority_provider_keys(self): + with patch.dict(os.environ, {'OPENAI_API_KEY': 'fixture-other', 'AZURE_OPENAI_API_KEY': 'fixture-azure'}): + self.post() + self.assertEqual(self.requests[-1].url.host, 'api.minimax.io') + with patch.dict(os.environ, {'OPENAI_API_KEY': 'fixture-other'}, clear=True): + self.assertEqual(server.default_model(), 'gpt-4o-mini') + self.post({'model': 'other-model'}) + self.assertEqual(self.requests[-1].url.host, 'api.openai.com') + + def test_proxy_catalog_preserves_aliases_and_nontext(self): + config = {'providers': [{'name': 'minimax', 'base_url': 'https://api.minimax.io/v1', + 'api_key': 'fixture-key', 'model_map': {'saved': 'MiniMax-M2.7'}}]} + with patch('optillm.plugins.proxy.client.OpenAI', side_effect=lambda **kwargs: OpenAI(http_client=self.http, **kwargs)): + models = list_provider_models(config) + ids = [model['id'] for model in models] + self.assertEqual(ids[:2], list(server.MINIMAX_MODELS)) + self.assertNotIn('MiniMax-M2.7', ids) + self.assertIn('speech-2.8-hd', ids) + self.assertIn('custom-model', ids) + self.assertIn('saved', ids) + + def test_proxy_fallback_serializes_direct_options(self): + from optillm.plugins import proxy_plugin + direct = OpenAI(api_key='fixture-key', base_url='https://api.minimax.io/v1', http_client=self.http) + with patch.object(proxy_plugin.ProxyConfig, 'load', return_value={'providers': []}): + proxy_plugin.run('', 'Hello', direct, 'MiniMax-M3.1-Flash', + request_config={'thinking': {'type': 'adaptive'}, 'reasoning_effort': 'max'}) + body = json.loads(self.requests[-1].content) + self.assertEqual(body['thinking'], {'type': 'adaptive'}) + self.assertEqual(body['reasoning_effort'], 'max') + + def test_proxy_transport_preserves_explicit_model_map(self): + config = {'providers': [{'name': 'minimax', 'base_url': 'https://api.minimaxi.com/v1', + 'api_key': 'fixture-key', 'model_map': {'saved': 'MiniMax-M3'}}], + 'routing': {'health_check': {'enabled': False}}} + with patch('optillm.plugins.proxy.client.OpenAI', side_effect=lambda **kwargs: OpenAI(http_client=self.http, **kwargs)): + client = ProxyClient(config) + client.chat.completions.create(model='saved', messages=[{'role': 'user', 'content': 'Hi'}], + thinking={'type': 'disabled'}) + body = json.loads(self.requests[-1].content) + self.assertEqual(body['model'], 'MiniMax-M3') + self.assertEqual(body['thinking'], {'type': 'disabled'}) + self.assertEqual(self.requests[-1].url.host, 'api.minimaxi.com') if __name__ == '__main__':