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
1 change: 1 addition & 0 deletions configs/_base_/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ model:
minimax_api_key: ""
minimax_model: "MiniMax-M3"
minimax_temperature: "0.7"
minimax_timeout_seconds: "300.0"
minimax_max_tokens: "8000"
minimax_enable_thinking: "false"
optimizer_minimax_base_url: "" # per-role override
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ winner. Prefer `thinking_mode`; use `disabled` when you need the field sent.
| `model.qwen_chat_*` | Shared `base_url`, `api_key`, `temperature`, `timeout_seconds`, `max_tokens`, `thinking_mode`, and the legacy `enable_thinking` |
| `model.qwen_chat_thinking_mode` | Wire policy for `chat_template_kwargs.enable_thinking`: `server_default` (default; omit the field), `enabled`, or `disabled`. See [Qwen thinking mode](#qwen-thinking-mode) |
| `model.optimizer_qwen_chat_*` / `model.target_qwen_chat_*` | Per-role Qwen overrides, including `*_qwen_chat_thinking_mode` |
| `model.minimax_*` | MiniMax `region`, `base_url`, `api_key`, shared `minimax_model`, `temperature`, `max_tokens`, and `enable_thinking`; `minimax_model` applies when MiniMax is the target |
| `model.minimax_*` | MiniMax `region`, `base_url`, `api_key`, shared `minimax_model`, `temperature`, `timeout_seconds`, `max_tokens`, and `enable_thinking`; `minimax_model` applies when MiniMax is the target |
| `model.codex_exec_*` | Codex path, sandbox, profile, SDK mode, reasoning, network/search, and approval policy; see compatibility notes below |
| `model.claude_code_exec_*` | Claude path, profile, SDK mode, effort, and thinking-token cap |
| `model.codex_trace_to_optimizer` | When `true` (default) and target is `codex_exec`, inject the agent's codex trace steps into the reflection prompt |
Expand Down
3 changes: 3 additions & 0 deletions scripts/eval_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--minimax_api_key", type=str)
p.add_argument("--minimax_model", type=str)
p.add_argument("--minimax_temperature", type=float)
p.add_argument("--minimax_timeout_seconds", type=float)
p.add_argument("--minimax_max_tokens", type=int)
p.add_argument("--minimax_enable_thinking", type=_BOOL)
p.add_argument("--out_root", type=str)
Expand Down Expand Up @@ -408,6 +409,7 @@ def load_config(args: argparse.Namespace) -> dict:
"minimax_api_key": "model.minimax_api_key",
"minimax_model": "model.minimax_model",
"minimax_temperature": "model.minimax_temperature",
"minimax_timeout_seconds": "model.minimax_timeout_seconds",
"minimax_max_tokens": "model.minimax_max_tokens",
"minimax_enable_thinking": "model.minimax_enable_thinking",
"seed": "train.seed",
Expand Down Expand Up @@ -692,6 +694,7 @@ def main() -> None:
base_url=cfg.get("minimax_base_url") or None,
api_key=cfg.get("minimax_api_key") or None,
temperature=cfg.get("minimax_temperature"),
timeout_seconds=cfg.get("minimax_timeout_seconds"),
max_tokens=cfg.get("minimax_max_tokens"),
enable_thinking=cfg.get("minimax_enable_thinking"),
)
Expand Down
2 changes: 2 additions & 0 deletions scripts/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--minimax_api_key", type=str)
p.add_argument("--minimax_model", type=str)
p.add_argument("--minimax_temperature", type=float)
p.add_argument("--minimax_timeout_seconds", type=float)
p.add_argument("--minimax_max_tokens", type=int)
p.add_argument("--minimax_enable_thinking", type=_BOOL)
p.add_argument("--openai_compatible_base_url", type=str)
Expand Down Expand Up @@ -442,6 +443,7 @@ def _retired_option_sources(
"minimax_api_key": "model.minimax_api_key",
"minimax_model": "model.minimax_model",
"minimax_temperature": "model.minimax_temperature",
"minimax_timeout_seconds": "model.minimax_timeout_seconds",
"minimax_max_tokens": "model.minimax_max_tokens",
"minimax_enable_thinking": "model.minimax_enable_thinking",
"openai_compatible_base_url": "model.openai_compatible_base_url",
Expand Down
1 change: 1 addition & 0 deletions skillopt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"model.minimax_api_key": "minimax_api_key",
"model.minimax_model": "minimax_model",
"model.minimax_temperature": "minimax_temperature",
"model.minimax_timeout_seconds": "minimax_timeout_seconds",
"model.minimax_max_tokens": "minimax_max_tokens",
"model.minimax_enable_thinking": "minimax_enable_thinking",
"model.openai_compatible_base_url": "openai_compatible_base_url",
Expand Down
1 change: 1 addition & 0 deletions skillopt/engine/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ def _build_eval_env(split: str, env_num: int, seed: int):
base_url=cfg.get("minimax_base_url") or None,
api_key=cfg.get("minimax_api_key") or None,
temperature=cfg.get("minimax_temperature"),
timeout_seconds=cfg.get("minimax_timeout_seconds"),
max_tokens=cfg.get("minimax_max_tokens"),
enable_thinking=cfg.get("minimax_enable_thinking"),
)
Expand Down
215 changes: 215 additions & 0 deletions tests/test_minimax_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"""Tests for MiniMax backend configuration and CLI pipeline."""

from __future__ import annotations

import tempfile
from unittest import mock

import pytest
import yaml

import scripts.eval_only as eval_only_script
import scripts.train as train_script
from skillopt.config import flatten_config, load_config


def test_flatten_config_maps_minimax_keys() -> None:
structured_cfg = {
"model": {
"backend": "minimax_chat",
"minimax_region": "global_en",
"minimax_base_url": "https://api.minimax.io/v1",
"minimax_api_key": "test-key",
"minimax_model": "MiniMax-M3",
"minimax_temperature": 0.7,
"minimax_timeout_seconds": 150.0,
"minimax_max_tokens": 4096,
"minimax_enable_thinking": True,
},
"train": {"num_epochs": 1},
"env": {"name": "searchqa"},
}

flat = flatten_config(structured_cfg)

assert flat["model_backend"] == "minimax_chat"
assert flat["minimax_region"] == "global_en"
assert flat["minimax_base_url"] == "https://api.minimax.io/v1"
assert flat["minimax_api_key"] == "test-key"
assert flat["minimax_model"] == "MiniMax-M3"
assert flat["minimax_temperature"] == 0.7
assert flat["minimax_timeout_seconds"] == 150.0
assert flat["minimax_max_tokens"] == 4096
assert flat["minimax_enable_thinking"] is True


def test_load_config_with_cfg_options_overrides() -> None:
raw = {
"model": {
"backend": "minimax_chat",
"minimax_model": "MiniMax-M3",
},
"env": {"name": "searchqa"},
}
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
yaml.dump(raw, f)
path = f.name

overrides = [
"model.minimax_timeout_seconds=45.0",
"model.minimax_temperature=0.2",
]
cfg = load_config(path, overrides=overrides)
flat = flatten_config(cfg)

assert flat["minimax_timeout_seconds"] == 45.0
assert flat["minimax_temperature"] == 0.2
assert flat["minimax_model"] == "MiniMax-M3"


def test_train_script_cli_to_structured_mapping() -> None:
cli_args = [
"--config", "configs/base.yaml",
"--minimax_region", "global_en",
"--minimax_base_url", "https://api.minimax.io/v1",
"--minimax_model", "MiniMax-M3",
"--minimax_temperature", "0.5",
"--minimax_timeout_seconds", "120.0",
"--minimax_max_tokens", "2048",
]

with mock.patch("sys.argv", ["train.py"] + cli_args):
args = train_script.parse_args()

assert args.minimax_region == "global_en"
assert args.minimax_base_url == "https://api.minimax.io/v1"
assert args.minimax_model == "MiniMax-M3"
assert args.minimax_temperature == 0.5
assert args.minimax_timeout_seconds == 120.0
assert args.minimax_max_tokens == 2048

for flag in [
"minimax_region",
"minimax_base_url",
"minimax_api_key",
"minimax_model",
"minimax_temperature",
"minimax_timeout_seconds",
"minimax_max_tokens",
"minimax_enable_thinking",
]:
assert flag in train_script._LEGACY_TO_STRUCTURED
assert train_script._LEGACY_TO_STRUCTURED[flag] == f"model.{flag}"


def test_eval_only_parse_args_and_map() -> None:
raw = {
"model": {
"backend": "minimax_chat",
},
"env": {"name": "searchqa"},
}
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
yaml.dump(raw, f)
config_path = f.name

cli_args = [
"--config", config_path,
"--skill", "skills/test.md",
"--backend", "minimax_chat",
"--minimax_timeout_seconds", "180.0",
]

with mock.patch("sys.argv", ["eval_only.py"] + cli_args):
args = eval_only_script.parse_args()
cfg = eval_only_script.load_config(args)

assert args.backend == "minimax_chat"
assert args.minimax_timeout_seconds == 180.0
assert cfg["minimax_timeout_seconds"] == 180.0


def test_eval_only_main_forwards_minimax_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
raw = {
"model": {
"backend": "minimax_chat",
"minimax_model": "MiniMax-M3",
"minimax_timeout_seconds": 180.0,
},
"env": {"name": "searchqa"},
}
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f_yaml:
yaml.dump(raw, f_yaml)
config_path = f_yaml.name

with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as f_skill:
f_skill.write("# Test Skill\n")
skill_path = f_skill.name

configured_kwargs: dict = {}

def fake_configure(**kwargs):
configured_kwargs.update(kwargs)

monkeypatch.setattr(eval_only_script, "configure_minimax_chat", fake_configure)

class EarlyExit(Exception):
pass

def stop_after_config(*args, **kwargs):
raise EarlyExit()

monkeypatch.setattr(eval_only_script, "set_reasoning_effort", stop_after_config)

cli_args = ["eval_only.py", "--config", config_path, "--skill", skill_path]
with mock.patch("sys.argv", cli_args):
with pytest.raises(EarlyExit):
eval_only_script.main()

assert configured_kwargs.get("timeout_seconds") == 180.0


def test_trainer_initialization_configures_minimax(monkeypatch: pytest.MonkeyPatch) -> None:
import skillopt.engine.trainer as trainer_mod
from skillopt.envs.base import EnvAdapter

configured_kwargs: dict = {}

def fake_configure(**kwargs):
configured_kwargs.update(kwargs)

monkeypatch.setattr(trainer_mod, "configure_minimax_chat", fake_configure)

class EarlyExit(Exception):
pass

def stop_after_model_config(*args, **kwargs):
raise EarlyExit()

monkeypatch.setattr(trainer_mod, "_configure_trace_to_optimizer_gates", stop_after_model_config)

cfg = {
"model_backend": "minimax_chat",
"optimizer_backend": "minimax_chat",
"target_backend": "minimax_chat",
"minimax_timeout_seconds": 210.0,
"skill_init": "skills/empty.md",
"num_epochs": 1,
"train_size": 1,
"batch_size": 1,
"accumulation": 1,
"merge_batch_size": 2,
"edit_budget": 2,
"seed": 42,
"out_root": "/tmp/out",
}

mock_adapter = mock.create_autospec(EnvAdapter, instance=True)
mock_adapter.requires_ray.return_value = False
mock_adapter.get_dataloader.return_value = None

trainer = trainer_mod.ReflACTTrainer(cfg, mock_adapter)
with pytest.raises(EarlyExit):
trainer.train()

assert configured_kwargs.get("timeout_seconds") == 210.0