diff --git a/README.md b/README.md
index 0727435..c26b53c 100644
--- a/README.md
+++ b/README.md
@@ -57,14 +57,18 @@
| ChatGLM-6B | text2vec-large-chinese |
| ChatGLM-6B-int8 | ernie-3.0-base-zh |
| ChatGLM-6B-int4 | ernie-3.0-nano-zh |
-| ChatGLM-6B-int4-qe | ernie-3.0-xbase-zh |
-| Vicuna-7b-1.1 | simbert-base-chinese |
-| Vicuna-13b-1.1 | paraphrase-multilingual-MiniLM-L12-v2 |
-| BELLE-LLaMA-7B-2M | |
-| BELLE-LLaMA-13B-2M | |
-| internlm-chat-7b-8k | |
-| internlm-chat-7b-v1_1 | |
-| internlm-chat-7b | |
+| ChatGLM-6B-int4-qe | ernie-3.0-xbase-zh |
+| Vicuna-7b-1.1 | simbert-base-chinese |
+| Vicuna-13b-1.1 | paraphrase-multilingual-MiniLM-L12-v2 |
+| BELLE-LLaMA-7B-2M | |
+| BELLE-LLaMA-13B-2M | |
+| internlm-chat-7b-8k | |
+| internlm-chat-7b-v1_1 | |
+| internlm-chat-7b | |
+| [MiniMax-M2.7](https://platform.minimaxi.com/) (Cloud API) | |
+| [MiniMax-M2.7-highspeed](https://platform.minimaxi.com/) (Cloud API) | |
+
+> **MiniMax Cloud LLM**: 除本地模型外,项目还支持通过 [MiniMax](https://platform.minimaxi.com/) 云端API调用大语言模型,无需GPU即可使用。设置环境变量 `MINIMAX_API_KEY` 后,在模型选择下拉框中选择 MiniMax-M2.7 或 MiniMax-M2.7-highspeed 即可。
## 💪 更新日志
diff --git a/app.py b/app.py
index 5927c35..2fd09b9 100644
--- a/app.py
+++ b/app.py
@@ -72,32 +72,36 @@ def init_model_config(
)
self.llm = None
torch.cuda.empty_cache()
- self.llm = ChatLLM()
- if 'chatglm2' in large_language_model.lower():
- self.llm.model_type = 'chatglm2'
- self.llm.model_name_or_path = llm_model_dict['chatglm2'][
- large_language_model]
- elif 'chatglm' in large_language_model.lower():
- self.llm.model_type = 'chatglm'
- self.llm.model_name_or_path = llm_model_dict['chatglm'][
- large_language_model]
- elif 'belle' in large_language_model.lower():
- self.llm.model_type = 'belle'
- self.llm.model_name_or_path = llm_model_dict['belle'][
- large_language_model]
- elif 'vicuna' in large_language_model.lower():
- self.llm.model_type = 'vicuna'
- self.llm.model_name_or_path = llm_model_dict['vicuna'][
- large_language_model]
- elif 'internlm' in large_language_model.lower():
- self.llm.model_type = 'internlm'
- self.llm.model_name_or_path = llm_model_dict['internlm'][
- large_language_model]
- elif 'yuan2' in large_language_model.lower():
- self.llm.model_type = 'yuan2'
- self.llm.model_name_or_path = llm_model_dict['yuan2'][large_language_model]
-
- self.llm.load_llm(llm_device=LLM_DEVICE, num_gpus=num_gpus)
+ if 'minimax' in large_language_model.lower():
+ from minimax_llm import ChatMiniMaxLLM
+ self.llm = ChatMiniMaxLLM(
+ model_name=llm_model_dict['minimax'][large_language_model])
+ else:
+ self.llm = ChatLLM()
+ if 'chatglm2' in large_language_model.lower():
+ self.llm.model_type = 'chatglm2'
+ self.llm.model_name_or_path = llm_model_dict['chatglm2'][
+ large_language_model]
+ elif 'chatglm' in large_language_model.lower():
+ self.llm.model_type = 'chatglm'
+ self.llm.model_name_or_path = llm_model_dict['chatglm'][
+ large_language_model]
+ elif 'belle' in large_language_model.lower():
+ self.llm.model_type = 'belle'
+ self.llm.model_name_or_path = llm_model_dict['belle'][
+ large_language_model]
+ elif 'vicuna' in large_language_model.lower():
+ self.llm.model_type = 'vicuna'
+ self.llm.model_name_or_path = llm_model_dict['vicuna'][
+ large_language_model]
+ elif 'internlm' in large_language_model.lower():
+ self.llm.model_type = 'internlm'
+ self.llm.model_name_or_path = llm_model_dict['internlm'][
+ large_language_model]
+ elif 'yuan2' in large_language_model.lower():
+ self.llm.model_type = 'yuan2'
+ self.llm.model_name_or_path = llm_model_dict['yuan2'][large_language_model]
+ self.llm.load_llm(llm_device=LLM_DEVICE, num_gpus=num_gpus)
def init_knowledge_vector_store(self, filepath):
diff --git a/config.py b/config.py
index e325ed9..3e873e0 100644
--- a/config.py
+++ b/config.py
@@ -57,8 +57,12 @@
"internlm-chat-7b-8k": "internlm/internlm-chat-7b-8k",
"internlm-chat-7b": "internlm/internlm-chat-7b",
"internlm-chat-7b-v1_1": "internlm/internlm-chat-7b-v1_1",
- }
+ },
"yuan2":{
"Yuan2-2B-hf":"IEITYuan/Yuan2-2B-hf"
+ },
+ "minimax": {
+ "MiniMax-M2.7": "MiniMax-M2.7",
+ "MiniMax-M2.7-highspeed": "MiniMax-M2.7-highspeed",
}
}
diff --git a/minimax_llm.py b/minimax_llm.py
new file mode 100644
index 0000000..c2c0bf2
--- /dev/null
+++ b/minimax_llm.py
@@ -0,0 +1,94 @@
+"""MiniMax Cloud LLM provider for LangChain-ChatGLM-Webui.
+
+Uses the MiniMax OpenAI-compatible API (https://api.minimax.io/v1) to provide
+cloud-based LLM inference as an alternative to locally-loaded models.
+"""
+
+import os
+import re
+from typing import Any, Dict, List, Optional
+
+from langchain.llms.base import LLM
+from langchain.llms.utils import enforce_stop_tokens
+from openai import OpenAI
+
+
+class ChatMiniMaxLLM(LLM):
+ """LangChain LLM wrapper for MiniMax cloud models via OpenAI-compatible API."""
+
+ model_name: str = "MiniMax-M2.7"
+ temperature: float = 0.1
+ max_token: int = 10000
+ top_p: float = 0.9
+ history: list = []
+ api_key: str = ""
+ api_base: str = "https://api.minimax.io/v1"
+ client: Any = None
+
+ def __init__(self, model_name: str = "MiniMax-M2.7", **kwargs):
+ super().__init__(**kwargs)
+ self.model_name = model_name
+ self.api_key = os.environ.get("MINIMAX_API_KEY", "")
+ if not self.api_key:
+ raise ValueError(
+ "MINIMAX_API_KEY environment variable is required. "
+ "Get your API key from https://platform.minimaxi.com/"
+ )
+ self._init_client()
+
+ def _init_client(self):
+ """Initialize the OpenAI client pointing to MiniMax API."""
+ self.client = OpenAI(
+ api_key=self.api_key,
+ base_url=self.api_base,
+ )
+
+ @property
+ def _llm_type(self) -> str:
+ return "ChatMiniMaxLLM"
+
+ def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
+ # MiniMax requires temperature in (0.0, 1.0]
+ temperature = max(0.01, min(self.temperature, 1.0))
+
+ messages = []
+ # Include conversation history
+ for h in self.history:
+ if h and len(h) == 2:
+ if h[0]:
+ messages.append({"role": "user", "content": h[0]})
+ if h[1]:
+ messages.append({"role": "assistant", "content": h[1]})
+ messages.append({"role": "user", "content": prompt})
+
+ response = self.client.chat.completions.create(
+ model=self.model_name,
+ messages=messages,
+ temperature=temperature,
+ max_tokens=self.max_token,
+ top_p=self.top_p,
+ )
+
+ result = response.choices[0].message.content or ""
+
+ # Strip thinking tags that MiniMax M2.7 may produce
+ result = re.sub(r".*?\s*", "", result, flags=re.DOTALL)
+
+ if stop is not None:
+ result = enforce_stop_tokens(result, stop)
+
+ self.history = self.history + [[None, result]]
+ return result
+
+ def load_llm(self, **kwargs):
+ """No-op for cloud models (no local model to load)."""
+ pass
+
+ @property
+ def _identifying_params(self) -> Dict[str, Any]:
+ return {
+ "model_name": self.model_name,
+ "api_base": self.api_base,
+ "temperature": self.temperature,
+ "max_token": self.max_token,
+ }
diff --git a/requirements.txt b/requirements.txt
index 9083edd..62c3778 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,3 +20,4 @@ langchain-serve # API版本需要
protobuf==4.25.2
langchain-community
einops
+openai>=1.0.0 # MiniMax Cloud LLM (OpenAI-compatible API)
diff --git a/tests/test_minimax_llm.py b/tests/test_minimax_llm.py
new file mode 100644
index 0000000..8e79750
--- /dev/null
+++ b/tests/test_minimax_llm.py
@@ -0,0 +1,434 @@
+"""Unit tests and integration tests for MiniMax LLM provider."""
+
+import os
+import re
+import sys
+import unittest
+from unittest.mock import MagicMock, patch, PropertyMock
+
+# Add parent directory to path so we can import minimax_llm
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+class TestChatMiniMaxLLMInit(unittest.TestCase):
+ """Test ChatMiniMaxLLM initialization and configuration."""
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_init_with_default_model(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ self.assertEqual(llm.model_name, "MiniMax-M2.7")
+ self.assertEqual(llm.api_base, "https://api.minimax.io/v1")
+ self.assertEqual(llm.api_key, "test-key-123")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_init_with_custom_model(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM(model_name="MiniMax-M2.7-highspeed")
+ self.assertEqual(llm.model_name, "MiniMax-M2.7-highspeed")
+
+ @patch.dict(os.environ, {}, clear=True)
+ def test_init_without_api_key_raises(self):
+ # Remove MINIMAX_API_KEY if present
+ os.environ.pop("MINIMAX_API_KEY", None)
+ from minimax_llm import ChatMiniMaxLLM
+ with self.assertRaises(ValueError) as ctx:
+ ChatMiniMaxLLM()
+ self.assertIn("MINIMAX_API_KEY", str(ctx.exception))
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_llm_type(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ self.assertEqual(llm._llm_type, "ChatMiniMaxLLM")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_identifying_params(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ params = llm._identifying_params
+ self.assertEqual(params["model_name"], "MiniMax-M2.7")
+ self.assertEqual(params["api_base"], "https://api.minimax.io/v1")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_load_llm_noop(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ # load_llm should be a no-op for cloud models
+ llm.load_llm() # Should not raise
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_openai_client_initialized(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ mock_openai.assert_called_once_with(
+ api_key="test-key-123",
+ base_url="https://api.minimax.io/v1",
+ )
+ self.assertIsNotNone(llm.client)
+
+
+class TestChatMiniMaxLLMCall(unittest.TestCase):
+ """Test ChatMiniMaxLLM _call method."""
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_basic_call(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "Hello, world!"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ result = llm._call("Say hello")
+ self.assertEqual(result, "Hello, world!")
+ llm.client.chat.completions.create.assert_called_once()
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_temperature_clamping_low(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ llm.temperature = 0.0 # Should be clamped to 0.01
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "result"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ llm._call("test")
+ call_kwargs = llm.client.chat.completions.create.call_args[1]
+ self.assertEqual(call_kwargs["temperature"], 0.01)
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_temperature_clamping_high(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ llm.temperature = 1.5 # Should be clamped to 1.0
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "result"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ llm._call("test")
+ call_kwargs = llm.client.chat.completions.create.call_args[1]
+ self.assertEqual(call_kwargs["temperature"], 1.0)
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_think_tag_stripping(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = (
+ "Let me think about this...\nActual answer here"
+ )
+ llm.client.chat.completions.create.return_value = mock_response
+
+ result = llm._call("test")
+ self.assertNotIn("", result)
+ self.assertEqual(result, "Actual answer here")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_stop_tokens(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "Hello STOP extra text"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ result = llm._call("test", stop=["STOP"])
+ self.assertNotIn("extra text", result)
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_history_appended(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ llm.history = []
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "Response 1"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ llm._call("Question 1")
+ self.assertEqual(len(llm.history), 1)
+ self.assertEqual(llm.history[0][1], "Response 1")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_history_included_in_messages(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ llm.history = [["previous question", "previous answer"]]
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "new answer"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ llm._call("new question")
+ call_kwargs = llm.client.chat.completions.create.call_args[1]
+ messages = call_kwargs["messages"]
+ self.assertEqual(len(messages), 3) # 1 user + 1 assistant from history + 1 new user
+ self.assertEqual(messages[0]["role"], "user")
+ self.assertEqual(messages[0]["content"], "previous question")
+ self.assertEqual(messages[1]["role"], "assistant")
+ self.assertEqual(messages[1]["content"], "previous answer")
+ self.assertEqual(messages[2]["role"], "user")
+ self.assertEqual(messages[2]["content"], "new question")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_none_content_returns_empty_string(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = None
+ llm.client.chat.completions.create.return_value = mock_response
+
+ result = llm._call("test")
+ self.assertEqual(result, "")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_model_passed_to_api(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM(model_name="MiniMax-M2.7-highspeed")
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "fast response"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ llm._call("test")
+ call_kwargs = llm.client.chat.completions.create.call_args[1]
+ self.assertEqual(call_kwargs["model"], "MiniMax-M2.7-highspeed")
+
+ @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
+ @patch("minimax_llm.OpenAI")
+ def test_max_tokens_passed(self, mock_openai):
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM()
+ llm.max_token = 2048
+
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "response"
+ llm.client.chat.completions.create.return_value = mock_response
+
+ llm._call("test")
+ call_kwargs = llm.client.chat.completions.create.call_args[1]
+ self.assertEqual(call_kwargs["max_tokens"], 2048)
+
+
+class TestConfigIntegration(unittest.TestCase):
+ """Test MiniMax integration with config.py."""
+
+ def test_minimax_in_llm_model_dict(self):
+ # Import config and verify MiniMax models are present
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ # Re-import to get fresh config
+ import importlib
+ # Avoid torch import issues by reading config directly
+ config_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "config.py"
+ )
+ with open(config_path) as f:
+ content = f.read()
+ self.assertIn('"minimax"', content)
+ self.assertIn('"MiniMax-M2.7"', content)
+ self.assertIn('"MiniMax-M2.7-highspeed"', content)
+
+ def test_config_syntax_valid(self):
+ """Verify config.py llm_model_dict is valid Python."""
+ config_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "config.py"
+ )
+ with open(config_path) as f:
+ content = f.read()
+ # Extract the llm_model_dict definition and verify it parses
+ start = content.index("llm_model_dict = {")
+ # Find matching closing brace
+ brace_count = 0
+ end = start
+ for i, c in enumerate(content[start:], start):
+ if c == '{':
+ brace_count += 1
+ elif c == '}':
+ brace_count -= 1
+ if brace_count == 0:
+ end = i + 1
+ break
+ dict_str = content[start + len("llm_model_dict = "):][:end - start - len("llm_model_dict = ")]
+ parsed = eval(dict_str)
+ self.assertIn("minimax", parsed)
+ self.assertEqual(parsed["minimax"]["MiniMax-M2.7"], "MiniMax-M2.7")
+
+
+class TestAppIntegration(unittest.TestCase):
+ """Test MiniMax integration in app.py model dispatch."""
+
+ def test_minimax_branch_in_app(self):
+ """Verify app.py contains MiniMax model dispatch."""
+ app_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "app.py"
+ )
+ with open(app_path) as f:
+ content = f.read()
+ self.assertIn("'minimax' in large_language_model.lower()", content)
+ self.assertIn("from minimax_llm import ChatMiniMaxLLM", content)
+
+ def test_minimax_models_in_dropdown_list(self):
+ """Verify MiniMax models appear in the model list."""
+ config_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "config.py"
+ )
+ with open(config_path) as f:
+ content = f.read()
+ # Parse the dict
+ start = content.index("llm_model_dict = {")
+ brace_count = 0
+ end = start
+ for i, c in enumerate(content[start:], start):
+ if c == '{':
+ brace_count += 1
+ elif c == '}':
+ brace_count -= 1
+ if brace_count == 0:
+ end = i + 1
+ break
+ dict_str = content[start + len("llm_model_dict = "):][:end - start - len("llm_model_dict = ")]
+ llm_model_dict = eval(dict_str)
+
+ # Build model list the same way app.py does
+ llm_model_list = []
+ for i in llm_model_dict:
+ for j in llm_model_dict[i]:
+ llm_model_list.append(j)
+
+ self.assertIn("MiniMax-M2.7", llm_model_list)
+ self.assertIn("MiniMax-M2.7-highspeed", llm_model_list)
+
+
+class TestReadmeIntegration(unittest.TestCase):
+ """Test README includes MiniMax documentation."""
+
+ def test_minimax_in_readme(self):
+ readme_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "README.md"
+ )
+ with open(readme_path) as f:
+ content = f.read()
+ self.assertIn("MiniMax-M2.7", content)
+ self.assertIn("MINIMAX_API_KEY", content)
+ self.assertIn("platform.minimaxi.com", content)
+
+
+class TestThinkTagRegex(unittest.TestCase):
+ """Test think tag stripping regex patterns."""
+
+ def test_simple_think_tag(self):
+ text = "thinking...\nAnswer"
+ result = re.sub(r".*?\s*", "", text, flags=re.DOTALL)
+ self.assertEqual(result, "Answer")
+
+ def test_multiline_think_tag(self):
+ text = "\nline1\nline2\n\n\nAnswer"
+ result = re.sub(r".*?\s*", "", text, flags=re.DOTALL)
+ self.assertEqual(result, "Answer")
+
+ def test_no_think_tag(self):
+ text = "Just a normal response"
+ result = re.sub(r".*?\s*", "", text, flags=re.DOTALL)
+ self.assertEqual(result, "Just a normal response")
+
+ def test_empty_think_tag(self):
+ text = "Answer"
+ result = re.sub(r".*?\s*", "", text, flags=re.DOTALL)
+ self.assertEqual(result, "Answer")
+
+
+# ===================== Integration Tests =====================
+
+class TestMiniMaxIntegration(unittest.TestCase):
+ """Integration tests that call the real MiniMax API.
+
+ These tests require MINIMAX_API_KEY to be set in the environment.
+ Skip if not available.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ api_key = os.environ.get("MINIMAX_API_KEY", "")
+ if not api_key:
+ # Try loading from .env.local
+ env_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.dirname(
+ os.path.dirname(os.path.abspath(__file__))))),
+ ".env.local"
+ )
+ if os.path.exists(env_path):
+ with open(env_path) as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("MINIMAX_API_KEY="):
+ api_key = line.split("=", 1)[1].strip().strip('"').strip("'")
+ os.environ["MINIMAX_API_KEY"] = api_key
+ break
+ if not api_key:
+ raise unittest.SkipTest("MINIMAX_API_KEY not set")
+
+ def test_basic_completion(self):
+ """Test basic chat completion with MiniMax API."""
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM(model_name="MiniMax-M2.7-highspeed")
+ llm.temperature = 0.01
+ llm.max_token = 100
+ result = llm._call("What is 2+2? Answer with just the number.")
+ self.assertIn("4", result)
+
+ def test_highspeed_model(self):
+ """Test MiniMax-M2.7-highspeed model."""
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM(model_name="MiniMax-M2.7-highspeed")
+ llm.temperature = 0.01
+ llm.max_token = 100
+ result = llm._call("What is the capital of France? Answer with just the city name.")
+ self.assertIn("Paris", result)
+
+ def test_chinese_response(self):
+ """Test Chinese language Q&A (matching project's primary use case)."""
+ from minimax_llm import ChatMiniMaxLLM
+ llm = ChatMiniMaxLLM(model_name="MiniMax-M2.7-highspeed")
+ llm.temperature = 0.01
+ llm.max_token = 200
+ result = llm._call("请用一句话回答:中国的首都是哪里?")
+ self.assertIn("北京", result)
+
+
+if __name__ == "__main__":
+ unittest.main()