Skip to content

Commit fca17aa

Browse files
committed
feat: add MiniMax as first-class LLM provider
- Add MiniMaxClient extending LLMClientBase with OpenAI-compatible API - Add base_minimax.yaml config template for MiniMax M2.7 - Add temperature clamping for MiniMax API constraints - Add 28 unit tests + 3 integration tests - Update README and documentation with MiniMax examples
1 parent fdcbc6b commit fca17aa

File tree

8 files changed

+646
-2
lines changed

8 files changed

+646
-2
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<div align="center">
1616
<strong>MiroFlow</strong> is the open-source agent framework that maximizes any model's agent performance — and proves it across 9+ benchmarks with reproducible results.<br>
17-
Plug in GPT-5, Claude, <a href="https://github.com/MiroMindAI/mirothinker">MiroThinker</a>, Kimi, DeepSeek, or any OpenAI-compatible model. Same tools. Same environment. Better results.
17+
Plug in GPT-5, Claude, <a href="https://github.com/MiroMindAI/mirothinker">MiroThinker</a>, Kimi, DeepSeek, <a href="https://platform.minimax.io">MiniMax</a>, or any OpenAI-compatible model. Same tools. Same environment. Better results.
1818
</div>
1919

2020
<br>
@@ -52,7 +52,7 @@ Plug in GPT-5, Claude, <a href="https://github.com/MiroMindAI/mirothinker">MiroT
5252
## Why MiroFlow
5353

5454
### Make Any Model Better
55-
- **Model-Agnostic Performance**: Plug in any LLM — GPT-5, Claude, MiroThinker, Kimi K2.5, DeepSeek — and get better agent performance through smart rollback, iterative reasoning, and optimized tool orchestration.
55+
- **Model-Agnostic Performance**: Plug in any LLM — GPT-5, Claude, MiroThinker, Kimi K2.5, DeepSeek, MiniMax — and get better agent performance through smart rollback, iterative reasoning, and optimized tool orchestration.
5656
- **Comprehensive Benchmarking**: Supports 9+ benchmarks including FutureX, GAIA, HLE, xBench-DeepSearch, BrowseComp, and more.
5757
- **One-Line Model Switching**: Change `provider_class` and `model_name` in YAML. Same tools, same prompts, same environment.
5858

@@ -115,6 +115,11 @@ llm:
115115
llm:
116116
provider_class: MiroThinkerSGLangClient
117117
model_name: mirothinker-v1.5
118+
119+
# MiniMax M2.7
120+
llm:
121+
provider_class: MiniMaxClient
122+
model_name: MiniMax-M2.7
118123
```
119124
120125
See [full documentation](https://miromindai.github.io/miroflow/quickstart/) for web app setup, more examples, and configuration options.

config/llm/base_minimax.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# config/llm/base_minimax.yaml
2+
# MiniMax M2.7 via OpenAI-compatible API
3+
# API docs: https://platform.minimax.io/docs/api-reference/text-openai-api
4+
provider_class: "MiniMaxClient"
5+
model_name: "MiniMax-M2.7"
6+
api_key: ${oc.env:MINIMAX_API_KEY,???}
7+
base_url: ${oc.env:MINIMAX_BASE_URL,https://api.minimax.io/v1}
8+
9+
temperature: 1.0
10+
top_p: 0.95
11+
min_p: 0.0
12+
top_k: -1
13+
14+
max_tokens: 32000
15+
max_context_length: 204800
16+
async_client: true
17+
18+
reasoning_effort: null
19+
repetition_penalty: 1.0
20+
21+
disable_cache_control: true
22+
keep_tool_result: -1
23+
24+
use_tool_calls: false

docs/mkdocs/docs/contribute_llm_clients.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ See existing providers in `miroflow/llm/`:
7171
- `OpenRouterClient` (`openrouter.py`) - Generic OpenRouter client
7272
- `OpenAIClient` (`openai_client.py`) - Generic OpenAI-compatible client
7373
- `MiroThinkerSGLangClient` (`mirothinker_sglang.py`) - MiroThinker via SGLang
74+
- `MiniMaxClient` (`minimax_client.py`) - MiniMax M2.7 via OpenAI-compatible API
7475

7576
---
7677

docs/mkdocs/docs/llm_clients_overview.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ MiroFlow is model-agnostic — plug in any LLM and get better agent performance
1313
| `ClaudeOpenRouterClient` | OpenRouter | anthropic/claude-3.7-sonnet, and other [supported models](https://openrouter.ai/models) || `OPENROUTER_API_KEY`, `OPENROUTER_BASE_URL` |
1414
| `OpenRouterClient` | OpenRouter | Any model on OpenRouter || `OPENROUTER_API_KEY`, `OPENROUTER_BASE_URL` |
1515
| `OpenAIClient` | OpenAI-Compatible | Any OpenAI-compatible model | GAIA Text-Only (Kimi K2.5) | `OPENAI_API_KEY`, `OPENAI_BASE_URL` |
16+
| `MiniMaxClient` | [MiniMax](https://platform.minimax.io) | MiniMax-M2.7, MiniMax-M2.7-highspeed || `MINIMAX_API_KEY`, `MINIMAX_BASE_URL` |
1617

1718
## Basic Configuration
1819

@@ -33,6 +34,7 @@ Pre-configured base configurations are available in `config/llm/`:
3334
| `base_mirothinker.yaml` | SGLang | MiroThinker model via SGLang |
3435
| `base_openai.yaml` | OpenAI | GPT models via OpenAI API |
3536
| `base_kimi_k25.yaml` | OpenAI-Compatible | Kimi K2.5 model |
37+
| `base_minimax.yaml` | MiniMax | MiniMax M2.7 via OpenAI-compatible API |
3638

3739
## Quick Setup
3840

@@ -69,6 +71,12 @@ main_agent:
6971
llm:
7072
provider_class: MiroThinkerSGLangClient
7173
model_name: mirothinker-v1.5
74+
75+
# MiniMax M2.7
76+
main_agent:
77+
llm:
78+
provider_class: MiniMaxClient
79+
model_name: MiniMax-M2.7
7280
```
7381

7482
See the [Model Comparison Leaderboard](model_comparison.md) for cross-model benchmark results.

miroflow/llm/minimax_client.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# SPDX-FileCopyrightText: 2025 MiromindAI
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""
6+
MiniMax LLM client - OpenAI-compatible provider for MiniMax M2.7 models.
7+
8+
Supported models:
9+
- MiniMax-M2.7: Peak performance, ultimate value (default)
10+
- MiniMax-M2.7-highspeed: Same performance, faster and more agile
11+
12+
API docs: https://platform.minimax.io/docs/api-reference/text-openai-api
13+
"""
14+
15+
from typing import Any, Dict, List
16+
17+
from omegaconf import DictConfig
18+
from openai import AsyncOpenAI, OpenAI
19+
from tenacity import retry, stop_after_attempt, wait_fixed
20+
21+
from miroflow.llm.base import LLMClientBase
22+
from miroflow.logging.task_tracer import get_tracer
23+
24+
logger = get_tracer()
25+
26+
# MiniMax models supported
27+
MINIMAX_MODELS = {"MiniMax-M2.7", "MiniMax-M2.7-highspeed"}
28+
29+
# MiniMax temperature range: (0.0, 1.0]
30+
MINIMAX_TEMP_MIN = 0.01
31+
MINIMAX_TEMP_MAX = 1.0
32+
33+
34+
def _clamp_temperature(temperature: float) -> float:
35+
"""Clamp temperature to MiniMax's valid range (0.0, 1.0]."""
36+
if temperature <= 0.0:
37+
return MINIMAX_TEMP_MIN
38+
if temperature > MINIMAX_TEMP_MAX:
39+
return MINIMAX_TEMP_MAX
40+
return temperature
41+
42+
43+
class MiniMaxClient(LLMClientBase):
44+
"""
45+
MiniMax LLM client using OpenAI-compatible API.
46+
47+
MiniMax provides high-performance language models accessible via
48+
an OpenAI-compatible endpoint at https://api.minimax.io/v1.
49+
50+
Configuration example (YAML):
51+
provider_class: "MiniMaxClient"
52+
model_name: "MiniMax-M2.7"
53+
api_key: ${oc.env:MINIMAX_API_KEY,???}
54+
base_url: ${oc.env:MINIMAX_BASE_URL,https://api.minimax.io/v1}
55+
"""
56+
57+
def _create_client(self, config: DictConfig):
58+
"""Create configured OpenAI-compatible client for MiniMax."""
59+
if self.async_client:
60+
return AsyncOpenAI(
61+
api_key=self.cfg.api_key,
62+
base_url=self.cfg.base_url,
63+
timeout=1800,
64+
)
65+
else:
66+
return OpenAI(
67+
api_key=self.cfg.api_key,
68+
base_url=self.cfg.base_url,
69+
timeout=1800,
70+
)
71+
72+
@retry(wait=wait_fixed(10), stop=stop_after_attempt(10))
73+
async def _create_message(
74+
self,
75+
system_prompt: str,
76+
messages: List[Dict[str, Any]],
77+
tools_definitions,
78+
keep_tool_result: int = -1,
79+
):
80+
"""Send message to MiniMax API via OpenAI-compatible endpoint."""
81+
logger.debug(f" Calling MiniMax LLM ({'async' if self.async_client else 'sync'})")
82+
83+
# Inject system prompt
84+
if system_prompt:
85+
if messages and messages[0]["role"] in ["system", "developer"]:
86+
messages[0] = {
87+
"role": "system",
88+
"content": [dict(type="text", text=system_prompt)],
89+
}
90+
else:
91+
messages.insert(
92+
0,
93+
{
94+
"role": "system",
95+
"content": [dict(type="text", text=system_prompt)],
96+
},
97+
)
98+
99+
messages_copy = self._remove_tool_result_from_messages(
100+
messages, keep_tool_result
101+
)
102+
103+
if tools_definitions:
104+
tool_list = await self.convert_tool_definition_to_tool_call(
105+
tools_definitions
106+
)
107+
else:
108+
tool_list = None
109+
110+
# Clamp temperature to MiniMax valid range
111+
temperature = _clamp_temperature(self.temperature)
112+
113+
params = {
114+
"model": self.model_name,
115+
"temperature": temperature,
116+
"max_completion_tokens": self.max_tokens,
117+
"messages": messages_copy,
118+
"tools": tool_list,
119+
"stream": False,
120+
}
121+
122+
if self.top_p != 1.0:
123+
params["top_p"] = self.top_p
124+
125+
try:
126+
if self.async_client:
127+
response = await self.client.chat.completions.create(**params)
128+
else:
129+
response = self.client.chat.completions.create(**params)
130+
131+
logger.debug(
132+
f"MiniMax LLM call status: {getattr(response.choices[0], 'finish_reason', 'N/A')}"
133+
)
134+
return response
135+
except Exception as e:
136+
logger.exception(f"MiniMax LLM call failed: {str(e)}")
137+
raise
138+
139+
def process_llm_response(self, llm_response) -> tuple[str, bool, dict]:
140+
"""Process MiniMax LLM response (OpenAI-compatible format)."""
141+
if not llm_response or not llm_response.choices:
142+
logger.debug("Error: MiniMax LLM did not return a valid response.")
143+
return "", True, {}
144+
145+
finish_reason = llm_response.choices[0].finish_reason
146+
147+
if finish_reason == "stop":
148+
text = llm_response.choices[0].message.content or ""
149+
return text, False, {"role": "assistant", "content": text}
150+
151+
if finish_reason == "tool_calls":
152+
tool_calls = llm_response.choices[0].message.tool_calls
153+
text = llm_response.choices[0].message.content or ""
154+
155+
if not text:
156+
descriptions = []
157+
for tc in tool_calls:
158+
descriptions.append(
159+
f"Using tool {tc.function.name} with arguments: {tc.function.arguments}"
160+
)
161+
text = "\n".join(descriptions)
162+
163+
assistant_message = {
164+
"role": "assistant",
165+
"content": text,
166+
"tool_calls": [
167+
{
168+
"id": tc.id,
169+
"type": "function",
170+
"function": {
171+
"name": tc.function.name,
172+
"arguments": tc.function.arguments,
173+
},
174+
}
175+
for tc in tool_calls
176+
],
177+
}
178+
return text, False, assistant_message
179+
180+
if finish_reason == "length":
181+
text = llm_response.choices[0].message.content or ""
182+
if text == "":
183+
text = "LLM response is empty. This is likely due to thinking block used up all tokens."
184+
return text, False, {"role": "assistant", "content": text}
185+
186+
raise ValueError(f"Unsupported finish reason: {finish_reason}")
187+
188+
def extract_tool_calls_info(self, llm_response, assistant_response_text):
189+
"""Extract tool call information from MiniMax response."""
190+
from miroflow.utils.parsing_utils import parse_llm_response_for_tool_calls
191+
192+
if llm_response.choices[0].finish_reason == "tool_calls":
193+
return parse_llm_response_for_tool_calls(
194+
llm_response.choices[0].message.tool_calls
195+
)
196+
return [], []
197+
198+
def update_message_history(
199+
self, message_history, tool_call_info, tool_calls_exceeded: bool = False
200+
):
201+
"""Update message history with tool call results."""
202+
for cur_call_id, tool_result in tool_call_info:
203+
message_history.append(
204+
{
205+
"role": "tool",
206+
"tool_call_id": cur_call_id,
207+
"content": tool_result["text"],
208+
}
209+
)
210+
return message_history
211+
212+
def handle_max_turns_reached_summary_prompt(self, message_history, summary_prompt):
213+
"""Handle max turns reached summary prompt."""
214+
return summary_prompt

tests/__init__.py

Whitespace-only changes.

tests/llm/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)