diff --git a/README.md b/README.md
index 4876c112f..1cd35a7ee 100644
--- a/README.md
+++ b/README.md
@@ -253,7 +253,7 @@ OpenEvolve implements a sophisticated **evolutionary coding pipeline** that goes
Advanced LLM Integration
-- **Universal API**: Works with OpenAI, Google, Claude Code CLI, local models, and proxies
+- **Universal API**: Works with OpenAI, Google, MiniMax, Claude Code CLI, local models, and proxies
- **Intelligent Ensembles**: Weighted combinations with sophisticated fallback
- **Test-Time Compute**: Enhanced reasoning through proxy systems (see [OptiLLM setup](#llm-provider-setup))
- **Plugin Ecosystem**: Support for advanced reasoning plugins
@@ -332,6 +332,7 @@ docker run --rm -v $(pwd):/app ghcr.io/algorithmicsuperintelligence/openevolve:l
- **o3-mini**: ~$0.03-0.12 per iteration (more cost-effective)
- **Gemini-2.5-Pro**: ~$0.08-0.30 per iteration
- **Gemini-2.5-Flash**: ~$0.01-0.05 per iteration (fastest and cheapest)
+- **MiniMax-M3.1-Flash / MiniMax-M3**: Cost depends on token usage; see [MiniMax pricing](https://platform.minimax.io/docs/pricing/overview).
- **Local models**: Nearly free after setup
- **OptiLLM**: Use cheaper models with test-time compute for better results
@@ -371,6 +372,33 @@ export OPENAI_API_KEY="your-gemini-api-key"
+
+🧠 MiniMax
+
+[MiniMax](https://platform.minimax.io/docs/api-reference/text-openai-api) provides MiniMax-M3.1-Flash and MiniMax-M3 with 1,000,000-token context windows via an OpenAI-compatible API. This configuration selects Flash by default:
+
+```yaml
+# config.yaml
+llm:
+ api_base: "https://api.minimax.io/v1"
+ api_key: "${MINIMAX_API_KEY}"
+ models:
+ - name: "MiniMax-M3.1-Flash"
+ weight: 1.0
+ - name: "MiniMax-M3"
+ weight: 0.0 # Increase to include M3 in the ensemble
+```
+
+```bash
+export MINIMAX_API_KEY="your-minimax-api-key"
+```
+
+> **Note:** For mainland China, set `api_base` to `https://api.minimaxi.com/v1` with a regional API key. Both models accept temperature in `[0, 2]`. Flash always thinks; optionally set `reasoning_effort` to `low`, `medium`, `high`, `xhigh`, or `max` (the API default). M3 ignores effort. Explicit model IDs in existing configurations are passed through unchanged.
+
+See [`configs/minimax_config.yaml`](configs/minimax_config.yaml) for a complete configuration example.
+
+
+
🏠 Local Models (Ollama/vLLM)
@@ -977,7 +1005,7 @@ See the [Cost Estimation](#cost-estimation) section in Installation & Setup for
**Yes!** OpenEvolve supports any OpenAI-compatible API:
-- **Commercial**: OpenAI, Google, Cohere
+- **Commercial**: OpenAI, Google, Cohere, MiniMax
- **Local**: Ollama, vLLM, LM Studio, text-generation-webui
- **Advanced**: OptiLLM for routing and test-time compute
diff --git a/configs/README.md b/configs/README.md
index 836c4b3ad..6b7dfba1b 100644
--- a/configs/README.md
+++ b/configs/README.md
@@ -12,6 +12,9 @@ The main configuration file containing all available options with sensible defau
Use this file as a template for your own configurations.
+### `minimax_config.yaml`
+A complete configuration for using [MiniMax](https://platform.minimax.io/docs/api-reference/text-openai-api) models with OpenEvolve. MiniMax-M3.1-Flash is the default for evolution and evaluation; increase MiniMax-M3's weight to include it in the evolution ensemble. Both models provide a 1,000,000-token context window through the OpenAI-compatible API. The configuration documents international and mainland China endpoints.
+
### `island_config_example.yaml`
A practical example configuration demonstrating proper island-based evolution setup. Shows:
- Recommended island settings for most use cases
diff --git a/configs/minimax_config.yaml b/configs/minimax_config.yaml
new file mode 100644
index 000000000..b7cb5f68c
--- /dev/null
+++ b/configs/minimax_config.yaml
@@ -0,0 +1,77 @@
+# OpenEvolve Configuration for MiniMax
+# MiniMax-M3.1-Flash is the default; MiniMax-M3 is an optional ensemble member.
+# Get your API key from: https://platform.minimax.io/
+#
+# Set your API key:
+# export MINIMAX_API_KEY="your-minimax-api-key"
+
+# General settings
+max_iterations: 100
+checkpoint_interval: 10
+log_level: "INFO"
+random_seed: 42
+
+# LLM configuration for MiniMax
+llm:
+ api_base: "https://api.minimax.io/v1"
+ # For mainland China, use https://api.minimaxi.com/v1 with a regional API key.
+ api_key: "${MINIMAX_API_KEY}"
+
+ # Flash is selected by default. Increase M3's weight to include it in evolution.
+ models:
+ - name: "MiniMax-M3.1-Flash"
+ weight: 1.0
+ - name: "MiniMax-M3"
+ weight: 0.0
+
+ # MiniMax models for LLM feedback
+ evaluator_models:
+ - name: "MiniMax-M3.1-Flash"
+ weight: 1.0
+
+ # Generation parameters
+ # Flash always thinks. Optional reasoning_effort: low, medium, high, xhigh, max.
+ # Omit reasoning_effort to use the API default (max); M3 ignores this setting.
+ # Both models accept temperature in [0, 2].
+ temperature: 0.7
+ top_p: 0.95
+ max_tokens: 4096
+
+ # Request parameters
+ timeout: 120
+ retries: 3
+ retry_delay: 5
+
+# Evolution settings
+diff_based_evolution: true
+max_code_length: 10000
+
+# Prompt configuration
+prompt:
+ system_message: "You are an expert coder helping to improve programs through evolution."
+ evaluator_system_message: "You are an expert code reviewer."
+ num_top_programs: 3
+ num_diverse_programs: 2
+ use_template_stochasticity: true
+ include_artifacts: true
+
+# Database configuration
+database:
+ population_size: 1000
+ num_islands: 5
+ migration_interval: 50
+ migration_rate: 0.1
+ feature_dimensions:
+ - "complexity"
+ - "diversity"
+ feature_bins: 10
+
+# Evaluator configuration
+evaluator:
+ timeout: 300
+ cascade_evaluation: true
+ cascade_thresholds:
+ - 0.5
+ - 0.75
+ - 0.9
+ parallel_evaluations: 4
diff --git a/tests/test_minimax_config.py b/tests/test_minimax_config.py
new file mode 100644
index 000000000..3d0953e03
--- /dev/null
+++ b/tests/test_minimax_config.py
@@ -0,0 +1,171 @@
+"""Exercise the MiniMax configuration through the real SDK's HTTP boundary."""
+
+import json
+import os
+import unittest
+from contextlib import ExitStack
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import patch
+
+import openai
+
+try:
+ import httpx
+except ModuleNotFoundError:
+ import httpx2 as httpx
+
+from openevolve.config import Config, load_config
+from openevolve.controller import OpenEvolve
+from openevolve.llm.ensemble import LLMEnsemble
+
+CONFIG_PATH = Path(__file__).resolve().parents[1] / "configs" / "minimax_config.yaml"
+
+
+class TestMiniMaxConfig(unittest.IsolatedAsyncioTestCase):
+ def load_example(self):
+ with patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-for-validation"}):
+ return load_config(str(CONFIG_PATH))
+
+ async def capture_request(self, models, messages=None, controller_config=None, **kwargs):
+ requests = []
+
+ def respond(request):
+ requests.append(request)
+ return httpx.Response(
+ 200,
+ json={
+ "id": "test-completion",
+ "object": "chat.completion",
+ "created": 0,
+ "model": json.loads(request.content)["model"],
+ "choices": [
+ {
+ "index": 0,
+ "finish_reason": "stop",
+ "message": {"role": "assistant", "content": "result"},
+ }
+ ],
+ },
+ )
+
+ # Replace only the network transport: config loading, ensemble selection,
+ # request construction and OpenAI SDK serialization all run normally.
+ client_class = openai.AsyncOpenAI
+
+ def create_client(**kwargs):
+ return client_class(
+ **kwargs, http_client=httpx.AsyncClient(transport=httpx.MockTransport(respond))
+ )
+
+ with ExitStack() as stack:
+ stack.enter_context(
+ patch("openevolve.llm.openai.openai.AsyncOpenAI", side_effect=create_client)
+ )
+ if controller_config is not None:
+ output_dir = stack.enter_context(TemporaryDirectory())
+ stack.enter_context(patch.object(OpenEvolve, "_setup_logging"))
+ example = CONFIG_PATH.parents[1] / "examples" / "function_minimization"
+ controller = OpenEvolve(
+ str(example / "initial_program.py"),
+ str(example / "evaluator.py"),
+ controller_config,
+ output_dir=output_dir,
+ )
+ ensemble = (
+ controller.llm_evaluator_ensemble
+ if models is controller_config.llm.evaluator_models
+ else controller.llm_ensemble
+ )
+ clients = controller.llm_ensemble.models + controller.llm_evaluator_ensemble.models
+ else:
+ ensemble = LLMEnsemble(models)
+ clients = ensemble.models
+ try:
+ result = await ensemble.generate_with_context(
+ "system", messages or [{"role": "user", "content": "improve"}], **kwargs
+ )
+ self.assertEqual(result, "result")
+ finally:
+ for model in clients:
+ await model.client.close()
+ self.assertEqual(len(requests), 1)
+ return requests[0], json.loads(requests[0].content)
+
+ async def test_example_defaults_reach_both_regional_endpoints(self):
+ for base in ("https://api.minimax.io/v1", "https://api.minimaxi.com/v1"):
+ config = self.load_example()
+ self.assertEqual(
+ [model.name for model in config.llm.models],
+ ["MiniMax-M3.1-Flash", "MiniMax-M3"],
+ )
+ self.assertEqual([model.weight for model in config.llm.models], [1.0, 0.0])
+ config.llm.update_model_params({"api_base": base}, overwrite=True)
+ for models in (config.llm.models, config.llm.evaluator_models):
+ with self.subTest(base=base, models=models):
+ request, body = await self.capture_request(models, controller_config=config)
+ self.assertEqual(body["seed"], config.llm.random_seed)
+ self.assertIsInstance(body["seed"], int)
+ self.assertEqual(str(request.url), base + "/chat/completions")
+ self.assertEqual(body["model"], "MiniMax-M3.1-Flash")
+ self.assertEqual(body["max_tokens"], 4096)
+ self.assertEqual(body["temperature"], 0.7)
+ self.assertEqual(body["top_p"], 0.95)
+ self.assertFalse(body["stream"])
+ for field in (
+ "thinking",
+ "reasoning_effort",
+ "reasoning_split",
+ "service_tier",
+ ):
+ self.assertNotIn(field, body)
+
+ async def test_flash_effort_reaches_wire(self):
+ for effort in ("low", "medium", "high", "xhigh", "max"):
+ with self.subTest(effort=effort):
+ config = self.load_example()
+ config.llm.update_model_params({"reasoning_effort": effort}, overwrite=True)
+ _, body = await self.capture_request(config.llm.models)
+ self.assertEqual(body["reasoning_effort"], effort)
+ _, body = await self.capture_request(config.llm.models, reasoning_effort="low")
+ self.assertEqual(body["reasoning_effort"], "low")
+
+ async def test_m3_can_be_selected_explicitly(self):
+ config = self.load_example()
+ config.llm.models[0].weight = 0.0
+ config.llm.models[1].weight = 1.0
+ _, body = await self.capture_request(config.llm.models)
+ self.assertEqual(body["model"], "MiniMax-M3")
+ self.assertNotIn("reasoning_effort", body)
+
+ async def test_saved_legacy_custom_models_and_content_are_preserved(self):
+ messages = [
+ {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": "describe"},
+ {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}},
+ {"type": "video_url", "video_url": {"url": "mm_file://fixture"}},
+ ],
+ }
+ ]
+ for model in ("MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"):
+ for key, value in (("primary_model", model), ("models", [{"name": model}])):
+ with self.subTest(model=model, key=key):
+ config = Config.from_dict(
+ {
+ "llm": {
+ "api_base": "https://example.com/v1",
+ "api_key": "test-key-for-validation",
+ key: value,
+ }
+ }
+ )
+ request, body = await self.capture_request(config.llm.models, messages)
+ self.assertEqual(str(request.url), "https://example.com/v1/chat/completions")
+ self.assertEqual(body["model"], model)
+ self.assertEqual(body["messages"][1:], messages)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_valid_configs.py b/tests/test_valid_configs.py
index ec46bd6a4..d2e65182d 100644
--- a/tests/test_valid_configs.py
+++ b/tests/test_valid_configs.py
@@ -2,9 +2,9 @@
Confirming the validity of configuration files in project directories
"""
+import itertools
import os
import unittest
-import itertools
from unittest.mock import MagicMock, patch
from openevolve.config import Config, load_config
@@ -24,7 +24,13 @@ def collect_files(self):
config_files.append(os.path.join(root, file))
return config_files
- @patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key-for-validation"})
+ @patch.dict(
+ os.environ,
+ {
+ "ANTHROPIC_API_KEY": "test-key-for-validation",
+ "MINIMAX_API_KEY": "test-key-for-validation",
+ },
+ )
def test_import_config_files(self):
"""Attempt to import all config files"""
config_files = self.collect_files()