Skip to content

Commit 83ea72e

Browse files
octo-patchCaralHsi
andauthored
feat: upgrade MiniMax default model to M2.7 (#1291)
* feat: add MiniMax as a first-class LLM provider Add MiniMax LLM support via the OpenAI-compatible API, following the same pattern as the existing Qwen and DeepSeek providers. Changes: - Add MinimaxLLMConfig with api_key, api_base, extra_body fields - Add MinimaxLLM class inheriting from OpenAILLM - Register minimax backend in LLMFactory and LLMConfigFactory - Add minimax_config() to APIConfig with env var support (MINIMAX_API_KEY, MINIMAX_API_BASE) - Add minimax to backend_model dicts in product/user config - Add MiniMax example scenario in examples/basic_modules/llm.py - Add unit tests for config and LLM (generate, stream, think prefix) - Update .env.example and README with MiniMax provider info MiniMax API: https://api.minimax.io/v1 (OpenAI-compatible) Models: MiniMax-M2.5, MiniMax-M2.5-highspeed (204K context) * feat: upgrade MiniMax default model to M2.7 - Update default model from MiniMax-M2.5 to MiniMax-M2.7 in API config - Update example code to use MiniMax-M2.7 as default with M2.7-highspeed listed - Update unit tests to reference M2.7 and M2.7-highspeed models - Keep all previous models (M2.5, M2.5-highspeed) as available alternatives * fix: Update MemReader configuration with backup support Enhanced MemReader configuration to support backup client and general model. * fix: derive MinimaxLLMConfig from OpenAILLMConfig * Add backup configuration options to test_llm.py * Add backup configuration options to test_llm.py * backup options in test_minimax_config Restored backup configuration options in test_llm.py. --------- Co-authored-by: CaralHsi <caralhsi@gmail.com>
1 parent 759af30 commit 83ea72e

File tree

9 files changed

+231
-3
lines changed

9 files changed

+231
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ Full tutorial → [MemOS-Cloud-OpenClaw-Plugin](https://github.com/MemTensor/Mem
224224
2. Configure `docker/.env.example` and copy to `MemOS/.env`
225225
- The `OPENAI_API_KEY`,`MOS_EMBEDDER_API_KEY`,`MEMRADER_API_KEY` and others can be applied for through [`BaiLian`](https://bailian.console.aliyun.com/?spm=a2c4g.11186623.0.0.2f2165b08fRk4l&tab=api#/api).
226226
- Fill in the corresponding configuration in the `MemOS/.env` file.
227+
- Supported LLM providers: **OpenAI**, **Azure OpenAI**, **Qwen (DashScope)**, **DeepSeek**, **MiniMax**, **Ollama**, **HuggingFace**, **vLLM**. Set `MOS_CHAT_MODEL_PROVIDER` to select the backend (e.g., `openai`, `qwen`, `deepseek`, `minimax`).
227228
3. Start the service.
228229

229230
- Launch via Docker

docker/.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ MOS_MAX_TOKENS=2048
2525
# Top-P for LLM in the Product API
2626
MOS_TOP_P=0.9
2727
# LLM for the Product API backend
28-
MOS_CHAT_MODEL_PROVIDER=openai # openai | huggingface | vllm
28+
MOS_CHAT_MODEL_PROVIDER=openai # openai | huggingface | vllm | minimax
2929
OPENAI_API_KEY=sk-xxx # [required] when provider=openai
3030
OPENAI_API_BASE=https://api.openai.com/v1 # [required] base for the key
31+
# MiniMax LLM (when provider=minimax)
32+
# MINIMAX_API_KEY=your-minimax-api-key # [required] when provider=minimax
33+
# MINIMAX_API_BASE=https://api.minimax.io/v1 # base for MiniMax API
3134

3235
## MemReader / retrieval LLM
3336
MEMRADER_MODEL=gpt-4o-mini

examples/basic_modules/llm.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,37 @@
164164
print("Scenario 6:", resp)
165165

166166

167-
# Scenario 7: Using LLMFactory with Deepseek-chat + reasoning + CoT + streaming
167+
# Scenario 7: Using LLMFactory with MiniMax (OpenAI-compatible API)
168+
# Prerequisites:
169+
# 1. Get your API key from the MiniMax platform.
170+
# 2. Available models: MiniMax-M2.7 (flagship), MiniMax-M2.7-highspeed (low-latency),
171+
# MiniMax-M2.5, MiniMax-M2.5-highspeed.
172+
173+
cfg_mm = LLMConfigFactory.model_validate(
174+
{
175+
"backend": "minimax",
176+
"config": {
177+
"model_name_or_path": "MiniMax-M2.7",
178+
"api_key": "your-minimax-api-key",
179+
"api_base": "https://api.minimax.io/v1",
180+
"temperature": 0.7,
181+
"max_tokens": 1024,
182+
},
183+
}
184+
)
185+
llm = LLMFactory.from_config(cfg_mm)
186+
messages = [{"role": "user", "content": "Hello, who are you"}]
187+
resp = llm.generate(messages)
188+
print("Scenario 7:", resp)
189+
print("==" * 20)
190+
191+
print("Scenario 7 (streaming):\n")
192+
for chunk in llm.generate_stream(messages):
193+
print(chunk, end="")
194+
print("\n" + "==" * 20)
195+
196+
197+
# Scenario 8: Using LLMFactory with DeepSeek-chat + reasoning + CoT + streaming
168198

169199
cfg2 = LLMConfigFactory.model_validate(
170200
{
@@ -186,7 +216,7 @@
186216
"content": "Explain how to solve this problem step-by-step. Be explicit in your thinking process. Question: If a train travels from city A to city B at 60 mph and returns at 40 mph, what is its average speed for the entire trip? Let's think step by step.",
187217
},
188218
]
189-
print("Scenario 7:\n")
219+
print("Scenario 8:\n")
190220
for chunk in llm.generate_stream(messages):
191221
print(chunk, end="")
192222
print("==" * 20)

src/memos/api/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,20 @@ def qwen_config() -> dict[str, Any]:
284284
"remove_think_prefix": True,
285285
}
286286

287+
@staticmethod
288+
def minimax_config() -> dict[str, Any]:
289+
"""Get MiniMax configuration."""
290+
return {
291+
"model_name_or_path": os.getenv("MOS_CHAT_MODEL", "MiniMax-M2.7"),
292+
"temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.8")),
293+
"max_tokens": int(os.getenv("MOS_MAX_TOKENS", "8000")),
294+
"top_p": float(os.getenv("MOS_TOP_P", "0.9")),
295+
"top_k": int(os.getenv("MOS_TOP_K", "50")),
296+
"remove_think_prefix": True,
297+
"api_key": os.getenv("MINIMAX_API_KEY", "your-api-key-here"),
298+
"api_base": os.getenv("MINIMAX_API_BASE", "https://api.minimax.io/v1"),
299+
}
300+
287301
@staticmethod
288302
def vllm_config() -> dict[str, Any]:
289303
"""Get Qwen configuration."""
@@ -901,12 +915,14 @@ def get_product_default_config() -> dict[str, Any]:
901915
openai_config = APIConfig.get_openai_config()
902916
qwen_config = APIConfig.qwen_config()
903917
vllm_config = APIConfig.vllm_config()
918+
minimax_config = APIConfig.minimax_config()
904919
reader_config = APIConfig.get_reader_config()
905920

906921
backend_model = {
907922
"openai": openai_config,
908923
"huggingface": qwen_config,
909924
"vllm": vllm_config,
925+
"minimax": minimax_config,
910926
}
911927
backend = os.getenv("MOS_CHAT_MODEL_PROVIDER", "openai")
912928
mysql_config = APIConfig.get_mysql_config()
@@ -1024,13 +1040,15 @@ def create_user_config(user_name: str, user_id: str) -> tuple["MOSConfig", "Gene
10241040
openai_config = APIConfig.get_openai_config()
10251041
qwen_config = APIConfig.qwen_config()
10261042
vllm_config = APIConfig.vllm_config()
1043+
minimax_config = APIConfig.minimax_config()
10271044
mysql_config = APIConfig.get_mysql_config()
10281045
reader_config = APIConfig.get_reader_config()
10291046
backend = os.getenv("MOS_CHAT_MODEL_PROVIDER", "openai")
10301047
backend_model = {
10311048
"openai": openai_config,
10321049
"huggingface": qwen_config,
10331050
"vllm": vllm_config,
1051+
"minimax": minimax_config,
10341052
}
10351053
# Create MOSConfig
10361054
config_dict = {

src/memos/configs/llm.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ class DeepSeekLLMConfig(OpenAILLMConfig):
7272
)
7373

7474

75+
class MinimaxLLMConfig(OpenAILLMConfig):
76+
api_key: str = Field(..., description="API key for MiniMax")
77+
api_base: str = Field(
78+
default="https://api.minimax.io/v1",
79+
description="Base URL for MiniMax OpenAI-compatible API",
80+
)
81+
extra_body: Any = Field(default=None, description="Extra options for API")
82+
83+
7584
class AzureLLMConfig(BaseLLMConfig):
7685
base_url: str = Field(
7786
default="https://api.openai.azure.com/",
@@ -146,6 +155,7 @@ class LLMConfigFactory(BaseConfig):
146155
"huggingface_singleton": HFLLMConfig, # Add singleton support
147156
"qwen": QwenLLMConfig,
148157
"deepseek": DeepSeekLLMConfig,
158+
"minimax": MinimaxLLMConfig,
149159
"openai_new": OpenAIResponsesLLMConfig,
150160
}
151161

src/memos/llms/factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from memos.llms.base import BaseLLM
55
from memos.llms.deepseek import DeepSeekLLM
66
from memos.llms.hf import HFLLM
7+
from memos.llms.minimax import MinimaxLLM
78
from memos.llms.hf_singleton import HFSingletonLLM
89
from memos.llms.ollama import OllamaLLM
910
from memos.llms.openai import AzureLLM, OpenAILLM
@@ -25,6 +26,7 @@ class LLMFactory(BaseLLM):
2526
"vllm": VLLMLLM,
2627
"qwen": QwenLLM,
2728
"deepseek": DeepSeekLLM,
29+
"minimax": MinimaxLLM,
2830
"openai_new": OpenAIResponsesLLM,
2931
}
3032

src/memos/llms/minimax.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from memos.configs.llm import MinimaxLLMConfig
2+
from memos.llms.openai import OpenAILLM
3+
from memos.log import get_logger
4+
5+
6+
logger = get_logger(__name__)
7+
8+
9+
class MinimaxLLM(OpenAILLM):
10+
"""MiniMax LLM class via OpenAI-compatible API."""
11+
12+
def __init__(self, config: MinimaxLLMConfig):
13+
super().__init__(config)

tests/configs/test_llm.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
BaseLLMConfig,
33
HFLLMConfig,
44
LLMConfigFactory,
5+
MinimaxLLMConfig,
56
OllamaLLMConfig,
67
OpenAILLMConfig,
78
)
@@ -145,6 +146,42 @@ def test_hf_llm_config():
145146
check_config_instantiation_invalid(HFLLMConfig)
146147

147148

149+
def test_minimax_llm_config():
150+
check_config_base_class(
151+
MinimaxLLMConfig,
152+
required_fields=["model_name_or_path", "api_key"],
153+
optional_fields=[
154+
"temperature",
155+
"max_tokens",
156+
"top_p",
157+
"top_k",
158+
"api_base",
159+
"remove_think_prefix",
160+
"extra_body",
161+
"default_headers",
162+
"backup_client",
163+
"backup_api_key",
164+
"backup_api_base",
165+
"backup_model_name_or_path",
166+
"backup_headers",
167+
],
168+
)
169+
170+
check_config_instantiation_valid(
171+
MinimaxLLMConfig,
172+
{
173+
"model_name_or_path": "MiniMax-M2.7",
174+
"api_key": "test-key",
175+
"api_base": "https://api.minimax.io/v1",
176+
"temperature": 0.7,
177+
"max_tokens": 1024,
178+
"top_p": 0.9,
179+
},
180+
)
181+
182+
check_config_instantiation_invalid(MinimaxLLMConfig)
183+
184+
148185
def test_llm_config_factory():
149186
check_config_factory_class(
150187
LLMConfigFactory,

tests/llms/test_minimax.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import unittest
2+
3+
from types import SimpleNamespace
4+
from unittest.mock import MagicMock
5+
6+
from memos.configs.llm import MinimaxLLMConfig
7+
from memos.llms.minimax import MinimaxLLM
8+
9+
10+
class TestMinimaxLLM(unittest.TestCase):
11+
def test_minimax_llm_generate_with_and_without_think_prefix(self):
12+
"""Test MinimaxLLM generate method with and without <think> tag removal."""
13+
14+
# Simulated full content including <think> tag
15+
full_content = "Hello from MiniMax!"
16+
reasoning_content = "Thinking in progress..."
17+
18+
# Mock response object
19+
mock_response = MagicMock()
20+
mock_response.model_dump_json.return_value = '{"mock": "true"}'
21+
mock_response.choices[0].message.content = full_content
22+
mock_response.choices[0].message.reasoning_content = reasoning_content
23+
24+
# Config with think prefix preserved
25+
config_with_think = MinimaxLLMConfig.model_validate(
26+
{
27+
"model_name_or_path": "MiniMax-M2.7",
28+
"temperature": 0.7,
29+
"max_tokens": 512,
30+
"top_p": 0.9,
31+
"api_key": "sk-test",
32+
"api_base": "https://api.minimax.io/v1",
33+
"remove_think_prefix": False,
34+
}
35+
)
36+
llm_with_think = MinimaxLLM(config_with_think)
37+
llm_with_think.client.chat.completions.create = MagicMock(return_value=mock_response)
38+
39+
output_with_think = llm_with_think.generate([{"role": "user", "content": "Hello"}])
40+
self.assertEqual(output_with_think, f"<think>{reasoning_content}</think>{full_content}")
41+
42+
# Config with think tag removed
43+
config_without_think = config_with_think.model_copy(update={"remove_think_prefix": True})
44+
llm_without_think = MinimaxLLM(config_without_think)
45+
llm_without_think.client.chat.completions.create = MagicMock(return_value=mock_response)
46+
47+
output_without_think = llm_without_think.generate([{"role": "user", "content": "Hello"}])
48+
self.assertEqual(output_without_think, full_content)
49+
50+
def test_minimax_llm_generate_stream(self):
51+
"""Test MinimaxLLM generate_stream with content chunks."""
52+
53+
def make_chunk(delta_dict):
54+
# Create a simulated stream chunk with delta fields
55+
delta = SimpleNamespace(**delta_dict)
56+
choice = SimpleNamespace(delta=delta)
57+
return SimpleNamespace(choices=[choice])
58+
59+
# Simulate chunks: content only (MiniMax standard response)
60+
mock_stream_chunks = [
61+
make_chunk({"content": "Hello"}),
62+
make_chunk({"content": ", "}),
63+
make_chunk({"content": "MiniMax!"}),
64+
]
65+
66+
mock_chat_completions_create = MagicMock(return_value=iter(mock_stream_chunks))
67+
68+
config = MinimaxLLMConfig.model_validate(
69+
{
70+
"model_name_or_path": "MiniMax-M2.7",
71+
"temperature": 0.7,
72+
"max_tokens": 512,
73+
"top_p": 0.9,
74+
"api_key": "sk-test",
75+
"api_base": "https://api.minimax.io/v1",
76+
"remove_think_prefix": False,
77+
}
78+
)
79+
llm = MinimaxLLM(config)
80+
llm.client.chat.completions.create = mock_chat_completions_create
81+
82+
messages = [{"role": "user", "content": "Say hello"}]
83+
streamed = list(llm.generate_stream(messages))
84+
full_output = "".join(streamed)
85+
86+
self.assertEqual(full_output, "Hello, MiniMax!")
87+
88+
def test_minimax_llm_config_defaults(self):
89+
"""Test MinimaxLLMConfig default values."""
90+
config = MinimaxLLMConfig.model_validate(
91+
{
92+
"model_name_or_path": "MiniMax-M2.7",
93+
"api_key": "sk-test",
94+
}
95+
)
96+
self.assertEqual(config.api_base, "https://api.minimax.io/v1")
97+
self.assertEqual(config.temperature, 0.7)
98+
self.assertEqual(config.max_tokens, 8192)
99+
100+
def test_minimax_llm_config_custom_values(self):
101+
"""Test MinimaxLLMConfig with custom values."""
102+
config = MinimaxLLMConfig.model_validate(
103+
{
104+
"model_name_or_path": "MiniMax-M2.7-highspeed",
105+
"api_key": "sk-test",
106+
"api_base": "https://custom.api.minimax.io/v1",
107+
"temperature": 0.5,
108+
"max_tokens": 2048,
109+
}
110+
)
111+
self.assertEqual(config.model_name_or_path, "MiniMax-M2.7-highspeed")
112+
self.assertEqual(config.api_base, "https://custom.api.minimax.io/v1")
113+
self.assertEqual(config.temperature, 0.5)
114+
self.assertEqual(config.max_tokens, 2048)

0 commit comments

Comments
 (0)