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
2 changes: 1 addition & 1 deletion configs/_base_/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ model:
minimax_region: "" # global_en (default) or cn_zh; selects the base URL
minimax_base_url: "" # region base URL if blank
minimax_api_key: ""
minimax_model: "MiniMax-M3"
minimax_model: "MiniMax-M3.1-Flash" # MiniMax-M3 is also supported
minimax_temperature: "0.7"
minimax_max_tokens: "8000"
minimax_enable_thinking: "false"
Expand Down
13 changes: 10 additions & 3 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,16 @@ reports no token counts, so usage totals are zero.
It does not support caller-supplied tools or structured tool calls; environments
that require those features must use another chat backend.

The current MiniMax adapter has one shared deployment. Set
`model.minimax_model` when MiniMax is the target; a mixed-backend run cannot
independently select a MiniMax optimizer model and a different target model.
MiniMax defaults to `MiniMax-M3.1-Flash`; `MiniMax-M3` is also supported.
Set `model.minimax_model` for the target default, or explicitly select
`model.target` and `model.optimizer` for each role. Explicit legacy and custom
model IDs are preserved.

Flash always uses adaptive thinking, even when `model.minimax_enable_thinking`
is false. Its `model.reasoning_effort` accepts `low`, `medium`, `high`, `xhigh`,
or `max`; an empty setting leaves the API default (`max`). The shared YAML
configuration sets `medium`. MiniMax-M3 continues to honor
`model.minimax_enable_thinking` and ignores reasoning effort.

MiniMax is served from region-specific hosts. Select one with
`model.minimax_region` (or `MINIMAX_REGION`) instead of hardcoding a host:
Expand Down
11 changes: 7 additions & 4 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ selecting the generic OpenAI-compatible backend.
| `cursor_exec` | — | ✓ |
| `copilot_exec` | — | ✓ |

MiniMax currently has one shared deployment. `model.minimax_model` is applied
when MiniMax is the target; mixed-backend runs cannot independently choose a
MiniMax optimizer model and a different target model.
MiniMax defaults to `MiniMax-M3.1-Flash`, with `MiniMax-M3` available explicitly.
`model.minimax_model` supplies the target default; explicit `model.target` and
`model.optimizer` values take precedence for their respective roles.
Flash always uses adaptive thinking and accepts reasoning effort `low`, `medium`,
`high`, `xhigh`, or `max`; empty effort uses the API default (`max`). MiniMax-M3
honors `model.minimax_enable_thinking` and ignores effort.
`model.minimax_region` selects the service region: `global_en` (default)
resolves to `https://api.minimax.io/v1` and `cn_zh` resolves to
`https://api.minimaxi.com/v1`. `model.minimax_base_url` overrides it.
Expand Down Expand Up @@ -97,7 +100,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`, default `minimax_model`, `temperature`, `max_tokens`, and `enable_thinking`; `minimax_model` applies when MiniMax is the target and no explicit target model is set |
| `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
8 changes: 7 additions & 1 deletion scripts/eval_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,12 @@ def _set_role(key: str, value: str) -> None:
and not _has_model_override("model.optimizer", "optimizer_model")
):
cfg["optimizer_model"] = default_model_for_backend("qwen_chat")
if cfg.get("optimizer_backend") == "minimax_chat":
if (
(not cfg.get("optimizer_model") or str(cfg.get("optimizer_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS)
and not _has_model_override("model.optimizer", "optimizer_model")
):
cfg["optimizer_model"] = default_model_for_backend("minimax_chat")
if cfg.get("optimizer_backend") == "openai_compatible":
if (
(not cfg.get("optimizer_model") or str(cfg.get("optimizer_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS)
Expand Down Expand Up @@ -697,7 +703,7 @@ def main() -> None:
)
minimax_model_cfg = cfg.get("minimax_model")
if minimax_model_cfg and cfg.get("target_backend") == "minimax_chat":
set_target_deployment(str(minimax_model_cfg))
set_target_deployment(str(cfg.get("target_model") or minimax_model_cfg))
configure_openai_compatible(
base_url=cfg.get("openai_compatible_base_url") or None,
api_key=cfg.get("openai_compatible_api_key") or None,
Expand Down
6 changes: 6 additions & 0 deletions scripts/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,12 @@ def _set_role(key: str, value: str) -> None:
and not _has_model_override("model.optimizer", "optimizer_model")
):
flat["optimizer_model"] = default_model_for_backend("qwen_chat")
if flat.get("optimizer_backend") == "minimax_chat":
if (
(not flat.get("optimizer_model") or str(flat.get("optimizer_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS)
and not _has_model_override("model.optimizer", "optimizer_model")
):
flat["optimizer_model"] = default_model_for_backend("minimax_chat")
if flat.get("optimizer_backend") == "openai_compatible":
if (
(not flat.get("optimizer_model") or str(flat.get("optimizer_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS)
Expand Down
2 changes: 1 addition & 1 deletion skillopt/engine/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,7 @@ def _build_eval_env(split: str, env_num: int, seed: int):
)
minimax_model_cfg = cfg.get("minimax_model")
if minimax_model_cfg and cfg.get("target_backend") == "minimax_chat":
set_target_deployment(str(minimax_model_cfg))
set_target_deployment(str(cfg.get("target_model") or minimax_model_cfg))
configure_openai_compatible(
base_url=cfg.get("openai_compatible_base_url") or None,
api_key=cfg.get("openai_compatible_api_key") or None,
Expand Down
7 changes: 6 additions & 1 deletion skillopt/envs/officeqa/rollout.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,10 @@ def _run_custom_search_process(
response = message.content or ""
final_response = response
last_response_metadata = _message_debug_metadata(message)
messages.append({"role": "assistant", "content": response})
assistant_message = {"role": "assistant", "content": response}
if get_target_backend() == "minimax_chat" and getattr(message, "reasoning_content", None) is not None:
assistant_message["reasoning_content"] = message.reasoning_content
messages.append(assistant_message)
message_event = {"type": "message", "turn": turn, "content": response}
if last_response_metadata:
message_event["response_metadata"] = last_response_metadata
Expand Down Expand Up @@ -663,6 +666,8 @@ def process_one(
response = message.content or ""
final_response = response
assistant_message = {"role": "assistant", "content": response}
if get_target_backend() == "minimax_chat" and getattr(message, "reasoning_content", None) is not None:
assistant_message["reasoning_content"] = message.reasoning_content
if getattr(message, "tool_calls", None):
assistant_message["tool_calls"] = [tool_call.model_dump(mode="json") for tool_call in message.tool_calls]
messages.append(assistant_message)
Expand Down
4 changes: 3 additions & 1 deletion skillopt/envs/spreadsheetbench/react_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import sys
import tempfile

from skillopt.model import chat_target_messages
from skillopt.model import chat_target_messages, get_target_backend
from skillopt.prompts import load_prompt
from skillopt.envs.spreadsheetbench.executor import generated_code_env

Expand Down Expand Up @@ -344,6 +344,8 @@ def _react_loop(
assistant_text = str(getattr(message, "content", "") or "")
tool_calls = _assistant_tool_calls(message)
assistant_payload: dict = {"role": "assistant", "content": assistant_text}
if get_target_backend() == "minimax_chat" and getattr(message, "reasoning_content", None) is not None:
assistant_payload["reasoning_content"] = message.reasoning_content
if tool_calls:
assistant_payload["tool_calls"] = tool_calls
messages.append(assistant_payload)
Expand Down
5 changes: 4 additions & 1 deletion skillopt/model/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
# deployments (not the CLI model, which uses the copilot_chat model keys)
# and keeps the openai_chat optimizer for copilot_exec non-empty.
"qwen_chat": "Qwen/Qwen3.5-4B",
"minimax_chat": "MiniMax-M3",
"minimax_chat": "MiniMax-M3.1-Flash",
"openai_compatible": "gpt-4o-mini",
}

Expand Down Expand Up @@ -167,10 +167,13 @@ class CompatAssistantMessage:
content: str
tool_calls: list[CompatToolCall] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
reasoning_content: str | None = None

def model_dump(self, mode: str = "json") -> dict[str, Any]:
del mode
data: dict[str, Any] = {"role": "assistant", "content": self.content}
if self.reasoning_content is not None:
data["reasoning_content"] = self.reasoning_content
if self.tool_calls:
data["tool_calls"] = [tool_call.model_dump() for tool_call in self.tool_calls]
return data
Expand Down
30 changes: 22 additions & 8 deletions skillopt/model/minimax_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ def base_url_for_region(region: str | None) -> str:
# keeps thinking on regardless, so sending "disabled" there is a lie we would
# then have to reason about downstream. Send the honest value instead.
_ALWAYS_THINKING_PREFIXES: tuple[str, ...] = ("MiniMax-M2",)
_FLASH_MODEL = "MiniMax-M3.1-Flash"
_FLASH_EFFORTS = frozenset({"low", "medium", "high", "xhigh", "max"})
REASONING_EFFORT: str | None = None


def _thinking_is_forced(deployment: str) -> bool:
"""True when ``deployment`` cannot honor ``thinking: {"type": "disabled"}``."""
name = str(deployment or "").strip()
return any(name.startswith(prefix) for prefix in _ALWAYS_THINKING_PREFIXES)
return name == _FLASH_MODEL or any(name.startswith(prefix) for prefix in _ALWAYS_THINKING_PREFIXES)


def _resolve_thinking_type(deployment: str) -> str:
Expand Down Expand Up @@ -159,6 +162,7 @@ def _compat_message_from_payload(message: dict[str, Any], choice: dict[str, Any]
return CompatAssistantMessage(
content=content,
tool_calls=tool_calls,
reasoning_content=message.get("reasoning_content"),
metadata={
"finish_reason": choice.get("finish_reason"),
"choice0": _json_safe(choice),
Expand Down Expand Up @@ -203,14 +207,22 @@ def _chat_messages_impl(
tool_choice: str | dict[str, Any] | None = None,
return_message: bool = False,
deployment: str | None = None,
reasoning_effort: str | None = None,
timeout: float | None = None,
) -> tuple[Any, dict[str, int]]:
model = deployment or TARGET_DEPLOYMENT
payload: dict[str, Any] = {
"model": deployment or TARGET_DEPLOYMENT,
"model": model,
"messages": _json_safe(messages),
"max_tokens": min(max_completion_tokens, MAX_TOKENS),
}
payload["thinking"] = {"type": _resolve_thinking_type(deployment or TARGET_DEPLOYMENT)}
payload["thinking"] = {"type": _resolve_thinking_type(model)}
if model == _FLASH_MODEL:
effort = REASONING_EFFORT if reasoning_effort is None else reasoning_effort
if effort:
if effort not in _FLASH_EFFORTS:
raise ValueError(f"Unsupported MiniMax-M3.1-Flash reasoning effort: {effort!r}")
payload["reasoning_effort"] = effort
if TEMPERATURE is not None:
payload["temperature"] = TEMPERATURE
if tools:
Expand Down Expand Up @@ -308,14 +320,14 @@ def chat_target(
reasoning_effort: str | None = None,
timeout: float | None = None,
) -> tuple[str, dict[str, int]]:
del reasoning_effort
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
return _chat_messages_impl(
messages,
max_completion_tokens,
retries,
stage,
deployment=TARGET_DEPLOYMENT,
reasoning_effort=reasoning_effort,
timeout=timeout,
)

Expand All @@ -336,14 +348,14 @@ def chat_optimizer(
(Azure) and produced "[skip] no usable patches" for any user running
optimizer+target on `minimax_chat`.
"""
del reasoning_effort
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
return _chat_messages_impl(
messages,
max_completion_tokens,
retries,
stage,
deployment=OPTIMIZER_DEPLOYMENT,
reasoning_effort=reasoning_effort,
timeout=timeout,
)

Expand All @@ -360,7 +372,6 @@ def chat_target_messages(
return_message: bool = False,
timeout: float | None = None,
) -> tuple[Any, dict[str, int]]:
del reasoning_effort
return _chat_messages_impl(
messages,
max_completion_tokens,
Expand All @@ -370,6 +381,7 @@ def chat_target_messages(
tool_choice=tool_choice,
return_message=return_message,
deployment=TARGET_DEPLOYMENT,
reasoning_effort=reasoning_effort,
timeout=timeout,
)

Expand All @@ -386,7 +398,6 @@ def chat_optimizer_messages(
return_message: bool = False,
timeout: float | None = None,
) -> tuple[Any, dict[str, int]]:
del reasoning_effort
return _chat_messages_impl(
messages,
max_completion_tokens,
Expand All @@ -396,6 +407,7 @@ def chat_optimizer_messages(
tool_choice=tool_choice,
return_message=return_message,
deployment=OPTIMIZER_DEPLOYMENT,
reasoning_effort=reasoning_effort,
timeout=timeout,
)

Expand All @@ -409,7 +421,9 @@ def reset_token_tracker() -> None:


def set_reasoning_effort(effort: str | None) -> None:
del effort
global REASONING_EFFORT
# Validate only when sending Flash: shared settings also reach other models.
REASONING_EFFORT = effort


def set_target_deployment(deployment: str) -> None:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_minimax_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ def test_default_deployment_is_current_model(monkeypatch: pytest.MonkeyPatch) ->
from skillopt.model import minimax_backend as backend
module = importlib.reload(backend)

assert default_model_for_backend("minimax_chat") == "MiniMax-M3"
assert module.TARGET_DEPLOYMENT == "MiniMax-M3"
assert module.OPTIMIZER_DEPLOYMENT == "MiniMax-M3"
assert default_model_for_backend("minimax_chat") == "MiniMax-M3.1-Flash"
assert module.TARGET_DEPLOYMENT == "MiniMax-M3.1-Flash"
assert module.OPTIMIZER_DEPLOYMENT == "MiniMax-M3.1-Flash"


def test_always_on_model_sends_adaptive_not_disabled(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
Expand Down Expand Up @@ -232,11 +232,11 @@ def test_fresh_import_optimizer_calls_without_setter(

text, usage = module.chat_optimizer("system prompt", "user query", retries=1)
assert text == "answer"
assert recorder.calls[0]["payload"]["model"] == "MiniMax-M3"
assert recorder.calls[0]["payload"]["model"] == "MiniMax-M3.1-Flash"

msg, usage_msg = module.chat_optimizer_messages([{"role": "user", "content": "user query"}], retries=1)
assert msg == "answer"
assert recorder.calls[1]["payload"]["model"] == "MiniMax-M3"
assert recorder.calls[1]["payload"]["model"] == "MiniMax-M3.1-Flash"


def test_fresh_import_respects_optimizer_deployment_env(
Expand Down
Loading