From 806b75bb6cf6901d3c2ab1c1d18b5f70b36541b8 Mon Sep 17 00:00:00 2001 From: hrushikeshramilla Date: Sat, 31 Oct 2026 16:43:03 +0530 Subject: [PATCH] fix(model): wire minimax_timeout_seconds across config, trainer, and CLI pipelines configure_minimax_chat supports timeout_seconds, but model.minimax_timeout_seconds was dropped during config flattening, lacked CLI flags in train.py and eval_only.py, and was not forwarded during trainer and eval initialization. This change: - Maps model.minimax_timeout_seconds in _FLATTEN_MAP (skillopt/config.py). - Adds --minimax_timeout_seconds flag and structured mapping in scripts/train.py and scripts/eval_only.py. - Forwards timeout_seconds to configure_minimax_chat in scripts/eval_only.py and skillopt/engine/trainer.py. - Documents the setting in configs/_base_/default.yaml and docs/reference/config.md. - Adds comprehensive unit tests in tests/test_minimax_config.py. --- configs/_base_/default.yaml | 1 + docs/reference/config.md | 2 +- scripts/eval_only.py | 3 + scripts/train.py | 2 + skillopt/config.py | 1 + skillopt/engine/trainer.py | 1 + tests/test_minimax_config.py | 215 +++++++++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/test_minimax_config.py diff --git a/configs/_base_/default.yaml b/configs/_base_/default.yaml index 4fe64088d..e874a1837 100644 --- a/configs/_base_/default.yaml +++ b/configs/_base_/default.yaml @@ -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 diff --git a/docs/reference/config.md b/docs/reference/config.md index 0645c0a67..9e6c74f65 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -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 | diff --git a/scripts/eval_only.py b/scripts/eval_only.py index e92c05c4b..b38d2bd53 100644 --- a/scripts/eval_only.py +++ b/scripts/eval_only.py @@ -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) @@ -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", @@ -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"), ) diff --git a/scripts/train.py b/scripts/train.py index 630f05625..801f34f5e 100644 --- a/scripts/train.py +++ b/scripts/train.py @@ -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) @@ -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", diff --git a/skillopt/config.py b/skillopt/config.py index 12230d31e..56e28d1e1 100644 --- a/skillopt/config.py +++ b/skillopt/config.py @@ -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", diff --git a/skillopt/engine/trainer.py b/skillopt/engine/trainer.py index 144e37dc2..6e43c5c1b 100644 --- a/skillopt/engine/trainer.py +++ b/skillopt/engine/trainer.py @@ -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"), ) diff --git a/tests/test_minimax_config.py b/tests/test_minimax_config.py new file mode 100644 index 000000000..0834c18ca --- /dev/null +++ b/tests/test_minimax_config.py @@ -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