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()