Skip to content

Commit 020bac1

Browse files
author
PR Bot
committed
feat: add MiniMax as first-class LLM provider
Add MiniMax (https://www.minimax.io) as a dedicated LLM backend adapter alongside OpenAI, Anthropic, and Gemini. MiniMax provides an OpenAI-compatible API with models like MiniMax-M2.7 (1M context) and MiniMax-M2.5-highspeed (204K context, optimized for speed). Changes: - Add MiniMaxAdapter in src/core/component/llm/llm_adapter/minimax_adapter.py with temperature clamping and <think> tag stripping for reasoning models - Register "minimax" provider in OpenAICompatibleClient factory - Add minimax backend config in llm_backends.yaml (M2.7, M2.7-highspeed, M2.5, M2.5-highspeed models) - Add MINIMAX_API_KEY env var in env.template - Update README.md and README.zh.md to list MiniMax as supported backend - Add 35 tests (32 unit + 3 integration) in tests/test_minimax_adapter.py
1 parent 3c9a2d0 commit 020bac1

7 files changed

Lines changed: 665 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ cp env.template .env
229229
# Edit .env and set:
230230
# - LLM_API_KEY (for memory extraction)
231231
# - VECTORIZE_API_KEY (for embedding/rerank)
232+
#
233+
# Supported LLM backends: OpenAI, Anthropic Claude, Google Gemini,
234+
# MiniMax, Azure OpenAI, Ollama, or any OpenAI-compatible API.
235+
# See src/config/llm_backends.yaml for full configuration details.
232236

233237
# 5. Start server
234238
uv run python src/run.py

README.zh.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ cp env.template .env
229229
# 编辑 .env 并设置:
230230
# - LLM_API_KEY(用于记忆提取)
231231
# - VECTORIZE_API_KEY(用于向量化 / rerank)
232+
#
233+
# 支持的 LLM 后端:OpenAI、Anthropic Claude、Google Gemini、
234+
# MiniMax、Azure OpenAI、Ollama 或任何 OpenAI 兼容 API。
235+
# 详细配置请参阅 src/config/llm_backends.yaml。
232236

233237
# 5. 启动服务
234238
uv run python src/run.py

env.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ LLM_MAX_TOKENS=32768
3030
# When using Qwen3 via OpenRouter, consider setting to "cerebras"
3131
# LLM_OPENROUTER_PROVIDER=cerebras
3232

33+
# ===================
34+
# MiniMax Configuration (optional, for using MiniMax as LLM backend)
35+
# ===================
36+
37+
# MINIMAX_API_KEY=your-minimax-api-key-here
38+
3339
# ===================
3440
# Vectorize (Embedding) Service Configuration
3541
# ===================

src/config/llm_backends.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ llm_backends:
5959
timeout: 600 # Increase to 10 minutes, suitable for time-consuming tasks like paper information extraction
6060
max_retries: 3
6161

62+
# MiniMax configuration
63+
minimax:
64+
name: "MiniMax"
65+
provider: "minimax"
66+
base_url: "https://api.minimax.io/v1"
67+
api_key: ""
68+
models:
69+
- "MiniMax-M2.7"
70+
- "MiniMax-M2.7-highspeed"
71+
- "MiniMax-M2.5"
72+
- "MiniMax-M2.5-highspeed"
73+
model: "MiniMax-M2.7"
74+
timeout: 600
75+
max_retries: 3
76+
6277
# Local Ollama configuration
6378
ollama:
6479
name: "Ollama Local"
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import re
2+
from typing import Dict, Any, List, Union, AsyncGenerator
3+
import os
4+
import openai
5+
from core.component.llm.llm_adapter.completion import (
6+
ChatCompletionRequest,
7+
ChatCompletionResponse,
8+
)
9+
from core.component.llm.llm_adapter.llm_backend_adapter import LLMBackendAdapter
10+
from core.constants.errors import ErrorMessage
11+
12+
13+
class MiniMaxAdapter(LLMBackendAdapter):
14+
"""MiniMax API adapter using OpenAI-compatible interface.
15+
16+
MiniMax provides an OpenAI-compatible API at https://api.minimax.io/v1.
17+
This adapter handles MiniMax-specific behaviors:
18+
- Temperature clamping to [0.01, 1.0] range
19+
- Stripping <think>...</think> tags from reasoning model responses
20+
- Auto-detection of MINIMAX_API_KEY environment variable
21+
"""
22+
23+
# MiniMax API requires temperature in (0.0, 1.0] for most models,
24+
# but temperature=0 is now accepted. We clamp to [0.01, 1.0] for safety
25+
# with older model versions.
26+
MIN_TEMPERATURE = 0.01
27+
MAX_TEMPERATURE = 1.0
28+
29+
# Pattern to strip thinking tags from reasoning model output
30+
_THINK_TAG_PATTERN = re.compile(
31+
r"<think>.*?</think>\s*", flags=re.DOTALL
32+
)
33+
34+
def __init__(self, config: Dict[str, Any]):
35+
self.config = config
36+
self.api_key = config.get("api_key") or os.getenv("MINIMAX_API_KEY")
37+
self.base_url = config.get(
38+
"base_url", "https://api.minimax.io/v1"
39+
)
40+
self.timeout = config.get("timeout", 600)
41+
42+
if not self.api_key:
43+
raise ValueError(ErrorMessage.INVALID_PARAMETER.value)
44+
45+
self.client = openai.AsyncOpenAI(
46+
api_key=self.api_key,
47+
base_url=self.base_url,
48+
timeout=self.timeout,
49+
)
50+
51+
@classmethod
52+
def _clamp_temperature(cls, temperature: float | None) -> float | None:
53+
"""Clamp temperature to MiniMax's accepted range."""
54+
if temperature is None:
55+
return None
56+
return max(cls.MIN_TEMPERATURE, min(cls.MAX_TEMPERATURE, temperature))
57+
58+
@classmethod
59+
def _strip_think_tags(cls, text: str) -> str:
60+
"""Strip <think>...</think> blocks from model output."""
61+
return cls._THINK_TAG_PATTERN.sub("", text).strip()
62+
63+
async def chat_completion(
64+
self, request: ChatCompletionRequest
65+
) -> Union[ChatCompletionResponse, AsyncGenerator[str, None]]:
66+
"""Perform chat completion via MiniMax OpenAI-compatible API."""
67+
if not request.model:
68+
raise ValueError(ErrorMessage.INVALID_PARAMETER.value)
69+
70+
params = request.to_dict()
71+
client_params = {
72+
"model": params.get("model"),
73+
"messages": params.get("messages"),
74+
"temperature": self._clamp_temperature(params.get("temperature")),
75+
"max_tokens": params.get("max_tokens"),
76+
"top_p": params.get("top_p"),
77+
"frequency_penalty": params.get("frequency_penalty"),
78+
"presence_penalty": params.get("presence_penalty"),
79+
"stream": params.get("stream", False),
80+
}
81+
final_params = {k: v for k, v in client_params.items() if v is not None}
82+
83+
try:
84+
if final_params.get("stream"):
85+
async def stream_gen():
86+
response_stream = await self.client.chat.completions.create(
87+
**final_params
88+
)
89+
async for chunk in response_stream:
90+
content = getattr(
91+
chunk.choices[0].delta, "content", None
92+
)
93+
if content:
94+
yield self._strip_think_tags(content)
95+
96+
return stream_gen()
97+
else:
98+
response = await self.client.chat.completions.create(
99+
**final_params
100+
)
101+
resp_dict = response.model_dump()
102+
# Strip think tags from non-streaming response
103+
for choice in resp_dict.get("choices", []):
104+
msg = choice.get("message", {})
105+
if msg.get("content"):
106+
msg["content"] = self._strip_think_tags(msg["content"])
107+
return ChatCompletionResponse.from_dict(resp_dict)
108+
except Exception as e:
109+
raise RuntimeError(
110+
f"MiniMax chat completion request failed: {e}"
111+
)
112+
113+
def get_available_models(self) -> List[str]:
114+
"""Get available MiniMax model list."""
115+
return self.config.get("models", [])

src/core/component/openai_compatible_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from core.component.llm.llm_adapter.openai_adapter import OpenAIAdapter
1616
from core.component.llm.llm_adapter.anthropic_adapter import AnthropicAdapter
1717
from core.component.llm.llm_adapter.gemini_adapter import GeminiAdapter
18+
from core.component.llm.llm_adapter.minimax_adapter import MiniMaxAdapter
1819

1920
logger = get_logger(__name__)
2021

@@ -75,6 +76,8 @@ async def _get_adapter(self, backend_name: str) -> LLMBackendAdapter:
7576
adapter = AnthropicAdapter(backend_config)
7677
elif provider == "gemini":
7778
adapter = GeminiAdapter(backend_config)
79+
elif provider == "minimax":
80+
adapter = MiniMaxAdapter(backend_config)
7881
else:
7982
raise ValueError(f"Unsupported provider type: {provider}")
8083

0 commit comments

Comments
 (0)