diff --git a/.github/benchmark-models.yml b/.github/benchmark-models.yml index be17b9a4..4fc2be69 100644 --- a/.github/benchmark-models.yml +++ b/.github/benchmark-models.yml @@ -33,6 +33,8 @@ models: #- openrouter/deepseek/deepseek-v3.2 # Minimax + - openrouter/minimax/minimax-m3 + - openrouter/minimax/minimax-m2.7 - openrouter/minimax/minimax-m2.1 - openrouter/minimax/minimax-m2.5 diff --git a/scripts/lib_agent.py b/scripts/lib_agent.py index ec2d3c48..b8c9e0b8 100644 --- a/scripts/lib_agent.py +++ b/scripts/lib_agent.py @@ -36,6 +36,53 @@ class ModelValidationError(Exception): # Valid thinking levels for OpenClaw reasoning depth VALID_THINKING_LEVELS = ("off", "minimal", "low", "medium", "high", "xhigh", "adaptive") +_MINIMAX_OPENAI_BASE_URLS = { + "https://api.minimax.io/v1", + "https://api.minimaxi.com/v1", +} + +_MINIMAX_MODEL_METADATA: dict[str, dict[str, Any]] = { + "MiniMax-M3": { + "reasoning": True, + "input": ["text", "image", "video"], + "contextWindow": 1_000_000, + "cost": { + "input": 0.6, + "output": 2.4, + "cacheRead": 0.12, + }, + }, + "MiniMax-M2.7": { + "reasoning": True, + "input": ["text"], + "contextWindow": 204_800, + "cost": { + "input": 0.3, + "output": 1.2, + "cacheRead": 0.06, + "cacheWrite": 0.375, + }, + }, +} + + +def _custom_model_definition(model_id: str, base_url: str) -> dict[str, Any]: + definition: dict[str, Any] = { + "id": model_id, + "name": model_id, + "reasoning": False, + "input": ["text"], + "contextWindow": 200_000, + "maxTokens": 8192, + } + + if base_url.rstrip("/") in _MINIMAX_OPENAI_BASE_URLS: + metadata = _MINIMAX_MODEL_METADATA.get(model_id) + if metadata: + definition.update(metadata) + + return definition + def _coerce_subprocess_output(value: Any) -> str: if value is None: @@ -325,16 +372,7 @@ def ensure_agent_exists( "baseUrl": base_url, "apiKey": key_ref, "api": "openai-completions", - "models": [ - { - "id": model_id, - "name": model_id, - "reasoning": False, - "input": ["text"], - "contextWindow": 200000, - "maxTokens": 8192, - } - ], + "models": [_custom_model_definition(model_id, base_url)], } data["defaultProvider"] = "custom" data["defaultModel"] = model_id diff --git a/tests/test_lib_agent_models.py b/tests/test_lib_agent_models.py new file mode 100644 index 00000000..64ee12d6 --- /dev/null +++ b/tests/test_lib_agent_models.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import json +import subprocess +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS_DIR = ROOT / "scripts" +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +from lib_agent import _custom_model_definition, ensure_agent_exists # noqa: E402 + + +class CustomModelDefinitionTests(unittest.TestCase): + def test_minimax_m3_uses_global_endpoint_metadata(self) -> None: + definition = _custom_model_definition( + "MiniMax-M3", + "https://api.minimax.io/v1", + ) + + self.assertEqual(definition["contextWindow"], 1_000_000) + self.assertEqual(definition["input"], ["text", "image", "video"]) + self.assertTrue(definition["reasoning"]) + self.assertEqual( + definition["cost"], + {"input": 0.6, "output": 2.4, "cacheRead": 0.12}, + ) + + def test_minimax_m27_uses_china_endpoint_metadata(self) -> None: + definition = _custom_model_definition( + "MiniMax-M2.7", + "https://api.minimaxi.com/v1/", + ) + + self.assertEqual(definition["contextWindow"], 204_800) + self.assertEqual(definition["input"], ["text"]) + self.assertTrue(definition["reasoning"]) + self.assertEqual( + definition["cost"], + { + "input": 0.3, + "output": 1.2, + "cacheRead": 0.06, + "cacheWrite": 0.375, + }, + ) + + def test_unknown_endpoint_keeps_generic_defaults(self) -> None: + definition = _custom_model_definition( + "MiniMax-M3", + "https://example.com/v1", + ) + + self.assertEqual( + definition, + { + "id": "MiniMax-M3", + "name": "MiniMax-M3", + "reasoning": False, + "input": ["text"], + "contextWindow": 200_000, + "maxTokens": 8192, + }, + ) + + def test_ensure_agent_writes_minimax_metadata(self) -> None: + completed = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") + + with TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + store_dir = root / "agents" / "bench-minimax-m3" + workspace = root / "workspace" + + with patch("lib_agent.subprocess.run", return_value=completed), patch( + "lib_agent._get_agent_store_dir", + return_value=store_dir, + ), patch("lib_agent.Path.home", return_value=root): + created = ensure_agent_exists( + "bench-minimax-m3", + "MiniMax-M3", + workspace, + base_url="https://api.minimax.io/v1", + api_key="test-key", + ) + + self.assertTrue(created) + config = json.loads((store_dir / "agent" / "models.json").read_text("utf-8")) + model = config["models"]["providers"]["custom"]["models"][0] + self.assertEqual( + model, + _custom_model_definition("MiniMax-M3", "https://api.minimax.io/v1"), + ) + + +if __name__ == "__main__": + unittest.main()