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
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ OpenEvolve implements a sophisticated **evolutionary coding pipeline** that goes
<details>
<summary><b>Advanced LLM Integration</b></summary>

- **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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -371,6 +372,33 @@ export OPENAI_API_KEY="your-gemini-api-key"

</details>

<details>
<summary><b>🧠 MiniMax</b></summary>

[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.

</details>

<details>
<summary><b>🏠 Local Models (Ollama/vLLM)</b></summary>

Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions configs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions configs/minimax_config.yaml
Original file line number Diff line number Diff line change
@@ -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
171 changes: 171 additions & 0 deletions tests/test_minimax_config.py
Original file line number Diff line number Diff line change
@@ -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()
10 changes: 8 additions & 2 deletions tests/test_valid_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Loading