Merge pull request #444 from ximiximi423/feat/add-minimax-provider

feat: add MiniMax as new LLM provider
This commit is contained in:
Martin
2026-03-16 19:57:55 +01:00
committed by GitHub
6 changed files with 2065 additions and 1589 deletions
+5 -1
View File
@@ -10,4 +10,8 @@ DEEPSEEK_API_KEY='xxxxx'
OPENROUTER_API_KEY='xxxxx' OPENROUTER_API_KEY='xxxxx'
TOGETHER_API_KEY='xxxxx' TOGETHER_API_KEY='xxxxx'
GOOGLE_API_KEY='xxxxx' GOOGLE_API_KEY='xxxxx'
ANTHROPIC_API_KEY='xxxxx' ANTHROPIC_API_KEY='xxxxx'
MINIMAX_API_KEY='xxxxx'
# Optional: MiniMax API base URL (default: https://api.minimax.io/v1)
# For mainland China users: https://api.minimaxi.com/v1
# MINIMAX_BASE_URL='https://api.minimax.io/v1'
+1
View File
@@ -240,6 +240,7 @@ provider_server_address = # Typically ignored or can be left blank when is_local
| Hugging Face | `huggingface` | No | Use models from Hugging Face Inference API. | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) | | Hugging Face | `huggingface` | No | Use models from Hugging Face Inference API. | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
| TogetherAI | `togetherAI` | No | Use various open-source models via TogetherAI API.| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) | | TogetherAI | `togetherAI` | No | Use various open-source models via TogetherAI API.| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) |
| OpenRouter | `openrouter` | No | Use OpenRouter Models| [https://openrouter.ai/](https://openrouter.ai/) | | OpenRouter | `openrouter` | No | Use OpenRouter Models| [https://openrouter.ai/](https://openrouter.ai/) |
| MiniMax | `minimax` | No | Use MiniMax M2.5 series models (e.g., MiniMax-M2.5).| [platform.minimax.io](https://platform.minimax.io/user-center/basic-information) |
*Note:* *Note:*
* We advise against using `gpt-4o` or other OpenAI models for complex web browsing and task planning as current prompt optimizations are geared towards models like Deepseek. * We advise against using `gpt-4o` or other OpenAI models for complex web browsing and task planning as current prompt optimizations are geared towards models like Deepseek.
+1
View File
@@ -230,6 +230,7 @@ provider_server_address = # 当 is_local = False 时,对于大多数 API 通
| Hugging Face | `huggingface` | 否 | 使用 Hugging Face Inference API 中的模型。 | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) | | Hugging Face | `huggingface` | 否 | 使用 Hugging Face Inference API 中的模型。 | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
| TogetherAI | `togetherAI` | 否 | 通过 TogetherAI API 使用各种开源模型。| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) | | TogetherAI | `togetherAI` | 否 | 通过 TogetherAI API 使用各种开源模型。| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) |
| OpenRouter | `openrouter` | No | 通过 OpenRouter 使用各种开源模型| [https://openrouter.ai/](https://openrouter.ai/) | | OpenRouter | `openrouter` | No | 通过 OpenRouter 使用各种开源模型| [https://openrouter.ai/](https://openrouter.ai/) |
| MiniMax | `minimax` | 否 | 使用 MiniMax 的 M2.5 系列模型(如 MiniMax-M2.5)。 | [platform.minimax.io](https://platform.minimax.io/user-center/basic-information) |
*注意:* *注意:*
* 我们不建议将 `gpt-4o` 或其他 OpenAI 模型用于复杂的网页浏览和任务规划,因为当前的提示优化针对 Deepseek 等模型。 * 我们不建议将 `gpt-4o` 或其他 OpenAI 模型用于复杂的网页浏览和任务规划,因为当前的提示优化针对 Deepseek 等模型。
+33 -1
View File
@@ -32,12 +32,13 @@ class Provider:
"together": self.together_fn, "together": self.together_fn,
"dsk_deepseek": self.dsk_deepseek, "dsk_deepseek": self.dsk_deepseek,
"openrouter": self.openrouter_fn, "openrouter": self.openrouter_fn,
"minimax": self.minimax_fn,
"test": self.test_fn "test": self.test_fn
} }
self.logger = Logger("provider.log") self.logger = Logger("provider.log")
self.api_key = None self.api_key = None
self.internal_url, self.in_docker = self.get_internal_url() self.internal_url, self.in_docker = self.get_internal_url()
self.unsafe_providers = ["openai", "deepseek", "dsk_deepseek", "together", "google", "openrouter"] self.unsafe_providers = ["openai", "deepseek", "dsk_deepseek", "together", "google", "openrouter", "minimax"]
if self.provider_name not in self.available_providers: if self.provider_name not in self.available_providers:
raise ValueError(f"Unknown provider: {provider_name}") raise ValueError(f"Unknown provider: {provider_name}")
if self.provider_name in self.unsafe_providers and self.is_local == False: if self.provider_name in self.unsafe_providers and self.is_local == False:
@@ -412,6 +413,37 @@ class Provider:
except Exception as e: except Exception as e:
raise Exception(f"OpenRouter API error: {str(e)}") from e raise Exception(f"OpenRouter API error: {str(e)}") from e
def minimax_fn(self, history, verbose=False):
"""
Use MiniMax API to generate text via OpenAI-compatible interface.
Supported models:
- MiniMax-M2.5: Peak performance model (~60 tps), 204,800 context window
- MiniMax-M2.5-highspeed: Same performance, faster (~100 tps)
Note: temperature must be in range (0.0, 1.0], default is 1.0
"""
load_dotenv()
base_url = os.getenv("MINIMAX_BASE_URL", "https://api.minimax.io/v1")
client = OpenAI(api_key=self.api_key, base_url=base_url)
if self.is_local:
raise Exception("MiniMax is not available for local use. Change config.ini")
try:
response = client.chat.completions.create(
model=self.model,
messages=history,
temperature=1.0,
)
if response is None:
raise Exception("MiniMax response is empty.")
thought = response.choices[0].message.content
if verbose:
print(thought)
return thought
except Exception as e:
raise Exception(f"MiniMax API error: {str(e)}") from e
def dsk_deepseek(self, history, verbose=False): def dsk_deepseek(self, history, verbose=False):
""" """
Use: xtekky/deepseek4free Use: xtekky/deepseek4free
+196
View File
@@ -0,0 +1,196 @@
import unittest
from unittest.mock import patch, MagicMock
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from sources.llm_provider import Provider
class TestMiniMaxProvider(unittest.TestCase):
"""Test cases for MiniMax provider integration."""
def test_minimax_provider_registered(self):
"""Test that minimax provider is registered in available_providers."""
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
self.assertIn("minimax", provider.available_providers)
def test_minimax_in_unsafe_providers(self):
"""Test that minimax is in unsafe_providers list."""
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
self.assertIn("minimax", provider.unsafe_providers)
def test_minimax_api_key_required(self):
"""Test that API key is fetched for minimax provider."""
with patch.object(Provider, 'get_api_key', return_value='test-minimax-key') as mock_get_key:
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
mock_get_key.assert_called_with("minimax")
self.assertEqual(provider.api_key, 'test-minimax-key')
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_local_not_supported(self, mock_openai_class):
"""Test that minimax provider raises error when is_local=True."""
provider = Provider("minimax", "MiniMax-M2.5", is_local=True)
provider.api_key = 'test-key'
history = [{"role": "user", "content": "Hello"}]
with self.assertRaises(Exception) as context:
provider.minimax_fn(history)
self.assertIn("not available for local use", str(context.exception))
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_uses_correct_base_url(self, mock_openai_class):
"""Test that minimax provider uses correct base URL."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Hello!"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
mock_openai_class.assert_called_with(
api_key='test-key',
base_url='https://api.minimax.io/v1'
)
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {
'MINIMAX_API_KEY': 'test-key',
'MINIMAX_BASE_URL': 'https://api.minimaxi.com/v1'
})
def test_minimax_custom_base_url(self, mock_openai_class):
"""Test that minimax provider uses custom base URL from env."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Hello!"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
mock_openai_class.assert_called_with(
api_key='test-key',
base_url='https://api.minimaxi.com/v1'
)
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_uses_temperature_one(self, mock_openai_class):
"""Test that minimax provider uses temperature=1.0."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Hello!"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['temperature'], 1.0)
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_returns_response_content(self, mock_openai_class):
"""Test that minimax provider returns response content."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Test response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
result = provider.minimax_fn(history)
self.assertEqual(result, "Test response")
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_handles_empty_response(self, mock_openai_class):
"""Test that minimax provider handles empty response."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_client.chat.completions.create.return_value = None
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
with self.assertRaises(Exception) as context:
provider.minimax_fn(history)
self.assertIn("response is empty", str(context.exception))
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_handles_api_error(self, mock_openai_class):
"""Test that minimax provider handles API errors."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_client.chat.completions.create.side_effect = Exception("API rate limit exceeded")
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
with self.assertRaises(Exception) as context:
provider.minimax_fn(history)
self.assertIn("MiniMax API error", str(context.exception))
class TestMiniMaxProviderModels(unittest.TestCase):
"""Test cases for MiniMax provider model configurations."""
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_m25_model(self, mock_openai_class):
"""Test MiniMax-M2.5 model."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['model'], "MiniMax-M2.5")
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_m25_highspeed_model(self, mock_openai_class):
"""Test MiniMax-M2.5-highspeed model."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5-highspeed", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['model'], "MiniMax-M2.5-highspeed")
if __name__ == '__main__':
unittest.main()
Generated
+1829 -1587
View File
File diff suppressed because it is too large Load Diff