Merge pull request #444 from ximiximi423/feat/add-minimax-provider
feat: add MiniMax as new LLM provider
This commit is contained in:
@@ -11,3 +11,7 @@ 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'
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user