Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<div align="center">
<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>
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.
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.
</div>

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

### Make Any Model Better
- **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.
- **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.
- **Comprehensive Benchmarking**: Supports 9+ benchmarks including FutureX, GAIA, HLE, xBench-DeepSearch, BrowseComp, and more.
- **One-Line Model Switching**: Change `provider_class` and `model_name` in YAML. Same tools, same prompts, same environment.

Expand Down Expand Up @@ -115,6 +115,11 @@ llm:
llm:
provider_class: MiroThinkerSGLangClient
model_name: mirothinker-v1.5

# MiniMax M2.7
llm:
provider_class: MiniMaxClient
model_name: MiniMax-M2.7
```

See [full documentation](https://miromindai.github.io/miroflow/quickstart/) for web app setup, more examples, and configuration options.
Expand Down
24 changes: 24 additions & 0 deletions config/llm/base_minimax.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# config/llm/base_minimax.yaml
# MiniMax M2.7 via OpenAI-compatible API
# API docs: https://platform.minimax.io/docs/api-reference/text-openai-api
provider_class: "MiniMaxClient"
model_name: "MiniMax-M2.7"
api_key: ${oc.env:MINIMAX_API_KEY,???}
base_url: ${oc.env:MINIMAX_BASE_URL,https://api.minimax.io/v1}

temperature: 1.0
top_p: 0.95
min_p: 0.0
top_k: -1

max_tokens: 32000
max_context_length: 204800
async_client: true

reasoning_effort: null
repetition_penalty: 1.0

disable_cache_control: true
keep_tool_result: -1

use_tool_calls: false
1 change: 1 addition & 0 deletions docs/mkdocs/docs/contribute_llm_clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ See existing providers in `miroflow/llm/`:
- `OpenRouterClient` (`openrouter.py`) - Generic OpenRouter client
- `OpenAIClient` (`openai_client.py`) - Generic OpenAI-compatible client
- `MiroThinkerSGLangClient` (`mirothinker_sglang.py`) - MiroThinker via SGLang
- `MiniMaxClient` (`minimax_client.py`) - MiniMax M2.7 via OpenAI-compatible API

---

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

## Basic Configuration

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

## Quick Setup

Expand Down Expand Up @@ -69,6 +71,12 @@ main_agent:
llm:
provider_class: MiroThinkerSGLangClient
model_name: mirothinker-v1.5

# MiniMax M2.7
main_agent:
llm:
provider_class: MiniMaxClient
model_name: MiniMax-M2.7
```

See the [Model Comparison Leaderboard](model_comparison.md) for cross-model benchmark results.
Expand Down
214 changes: 214 additions & 0 deletions miroflow/llm/minimax_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# SPDX-FileCopyrightText: 2025 MiromindAI
#
# SPDX-License-Identifier: Apache-2.0

"""
MiniMax LLM client - OpenAI-compatible provider for MiniMax M2.7 models.

Supported models:
- MiniMax-M2.7: Peak performance, ultimate value (default)
- MiniMax-M2.7-highspeed: Same performance, faster and more agile

API docs: https://platform.minimax.io/docs/api-reference/text-openai-api
"""

from typing import Any, Dict, List

from omegaconf import DictConfig
from openai import AsyncOpenAI, OpenAI
from tenacity import retry, stop_after_attempt, wait_fixed

from miroflow.llm.base import LLMClientBase
from miroflow.logging.task_tracer import get_tracer

logger = get_tracer()

# MiniMax models supported
MINIMAX_MODELS = {"MiniMax-M2.7", "MiniMax-M2.7-highspeed"}

# MiniMax temperature range: (0.0, 1.0]
MINIMAX_TEMP_MIN = 0.01
MINIMAX_TEMP_MAX = 1.0


def _clamp_temperature(temperature: float) -> float:
"""Clamp temperature to MiniMax's valid range (0.0, 1.0]."""
if temperature <= 0.0:
return MINIMAX_TEMP_MIN
if temperature > MINIMAX_TEMP_MAX:
return MINIMAX_TEMP_MAX
return temperature


class MiniMaxClient(LLMClientBase):
"""
MiniMax LLM client using OpenAI-compatible API.

MiniMax provides high-performance language models accessible via
an OpenAI-compatible endpoint at https://api.minimax.io/v1.

Configuration example (YAML):
provider_class: "MiniMaxClient"
model_name: "MiniMax-M2.7"
api_key: ${oc.env:MINIMAX_API_KEY,???}
base_url: ${oc.env:MINIMAX_BASE_URL,https://api.minimax.io/v1}
"""

def _create_client(self, config: DictConfig):
"""Create configured OpenAI-compatible client for MiniMax."""
if self.async_client:
return AsyncOpenAI(
api_key=self.cfg.api_key,
base_url=self.cfg.base_url,
timeout=1800,
)
else:
return OpenAI(
api_key=self.cfg.api_key,
base_url=self.cfg.base_url,
timeout=1800,
)

@retry(wait=wait_fixed(10), stop=stop_after_attempt(10))
async def _create_message(
self,
system_prompt: str,
messages: List[Dict[str, Any]],
tools_definitions,
keep_tool_result: int = -1,
):
"""Send message to MiniMax API via OpenAI-compatible endpoint."""
logger.debug(f" Calling MiniMax LLM ({'async' if self.async_client else 'sync'})")

# Inject system prompt
if system_prompt:
if messages and messages[0]["role"] in ["system", "developer"]:
messages[0] = {
"role": "system",
"content": [dict(type="text", text=system_prompt)],
}
else:
messages.insert(
0,
{
"role": "system",
"content": [dict(type="text", text=system_prompt)],
},
)

messages_copy = self._remove_tool_result_from_messages(
messages, keep_tool_result
)

if tools_definitions:
tool_list = await self.convert_tool_definition_to_tool_call(
tools_definitions
)
else:
tool_list = None

# Clamp temperature to MiniMax valid range
temperature = _clamp_temperature(self.temperature)

params = {
"model": self.model_name,
"temperature": temperature,
"max_completion_tokens": self.max_tokens,
"messages": messages_copy,
"tools": tool_list,
"stream": False,
}

if self.top_p != 1.0:
params["top_p"] = self.top_p

try:
if self.async_client:
response = await self.client.chat.completions.create(**params)
else:
response = self.client.chat.completions.create(**params)

logger.debug(
f"MiniMax LLM call status: {getattr(response.choices[0], 'finish_reason', 'N/A')}"
)
return response
except Exception as e:
logger.exception(f"MiniMax LLM call failed: {str(e)}")
raise

def process_llm_response(self, llm_response) -> tuple[str, bool, dict]:
"""Process MiniMax LLM response (OpenAI-compatible format)."""
if not llm_response or not llm_response.choices:
logger.debug("Error: MiniMax LLM did not return a valid response.")
return "", True, {}

finish_reason = llm_response.choices[0].finish_reason

if finish_reason == "stop":
text = llm_response.choices[0].message.content or ""
return text, False, {"role": "assistant", "content": text}

if finish_reason == "tool_calls":
tool_calls = llm_response.choices[0].message.tool_calls
text = llm_response.choices[0].message.content or ""

if not text:
descriptions = []
for tc in tool_calls:
descriptions.append(
f"Using tool {tc.function.name} with arguments: {tc.function.arguments}"
)
text = "\n".join(descriptions)

assistant_message = {
"role": "assistant",
"content": text,
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
for tc in tool_calls
],
}
return text, False, assistant_message

if finish_reason == "length":
text = llm_response.choices[0].message.content or ""
if text == "":
text = "LLM response is empty. This is likely due to thinking block used up all tokens."
return text, False, {"role": "assistant", "content": text}

raise ValueError(f"Unsupported finish reason: {finish_reason}")

def extract_tool_calls_info(self, llm_response, assistant_response_text):
"""Extract tool call information from MiniMax response."""
from miroflow.utils.parsing_utils import parse_llm_response_for_tool_calls

if llm_response.choices[0].finish_reason == "tool_calls":
return parse_llm_response_for_tool_calls(
llm_response.choices[0].message.tool_calls
)
return [], []

def update_message_history(
self, message_history, tool_call_info, tool_calls_exceeded: bool = False
):
"""Update message history with tool call results."""
for cur_call_id, tool_result in tool_call_info:
message_history.append(
{
"role": "tool",
"tool_call_id": cur_call_id,
"content": tool_result["text"],
}
)
return message_history

def handle_max_turns_reached_summary_prompt(self, message_history, summary_prompt):
"""Handle max turns reached summary prompt."""
return summary_prompt
Empty file added tests/__init__.py
Empty file.
Empty file added tests/llm/__init__.py
Empty file.
Loading