Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export AZURE_OPENAI_AUTH_MODE=api_key
# model.optimizer/model.target for the selected Qwen roles.

# ── MiniMax (for minimax_chat backend) ──────────────────────────────
# Pick the service region instead of hardcoding a host: global_en (default)
# resolves to https://api.minimax.io/v1 and cn_zh resolves to
# https://api.minimaxi.com/v1. MINIMAX_BASE_URL still overrides the region.
# export MINIMAX_REGION=global_en
# export MINIMAX_BASE_URL=https://api.minimax.io/v1
# export MINIMAX_API_KEY=...
# When MiniMax is the target, set model.minimax_model in YAML. The current
Expand Down
3 changes: 2 additions & 1 deletion configs/_base_/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ model:
target_azure_openai_managed_identity_client_id: ""

# MiniMax backend settings (minimax_chat target)
minimax_base_url: "" # https://api.minimax.io/v1 if blank
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-M2.7"
minimax_temperature: "0.7"
Expand Down
9 changes: 8 additions & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ 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 is served from region-specific hosts. Select one with
`model.minimax_region` (or `MINIMAX_REGION`) instead of hardcoding a host:
`global_en` (default) resolves to `https://api.minimax.io/v1` and `cn_zh`
resolves to `https://api.minimaxi.com/v1`. An explicit
`model.minimax_base_url` or `MINIMAX_BASE_URL` overrides the region default.

For a generic compatible provider, select the role backends explicitly rather
than relying on a high-level shorthand:

Expand Down Expand Up @@ -200,7 +206,8 @@ Model credentials are loaded from environment variables:
| `CURSOR_API_KEY` | `cursor_exec` | Optional authentication method understood directly by Cursor Agent |
| `QWEN_CHAT_BASE_URL` | `qwen_chat` | Local Qwen/vLLM endpoint |
| `QWEN_CHAT_MODEL` | `qwen_chat` | Served model name for direct library use; train/eval YAML role models take precedence |
| `MINIMAX_BASE_URL` | `minimax_chat` | MiniMax-compatible base URL |
| `MINIMAX_REGION` | `minimax_chat` | Service region: `global_en` (default) or `cn_zh`; selects the base URL |
| `MINIMAX_BASE_URL` | `minimax_chat` | MiniMax-compatible base URL; overrides the region default |
| `MINIMAX_API_KEY` | `minimax_chat` | MiniMax API key |
| `COPILOT_EXEC_PATH` | `copilot_chat`, `copilot_exec` | Optional path to `copilot`; defaults to `copilot` |
| `COPILOT_EXEC_HOME` | `copilot_chat`, `copilot_exec` | Optional `COPILOT_HOME` override isolating CLI config; sign-in state lives outside it |
Expand Down
5 changes: 4 additions & 1 deletion docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ selecting the generic OpenAI-compatible backend.
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.
`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.

| Parameter | Type | Default | Description |
|---|---|---|---|
Expand Down Expand Up @@ -64,7 +67,7 @@ defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`.
|---|---|
| `model.qwen_chat_*` | Shared `base_url`, `api_key`, `temperature`, `timeout_seconds`, `max_tokens`, and `enable_thinking` |
| `model.optimizer_qwen_chat_*` / `model.target_qwen_chat_*` | Per-role Qwen overrides |
| `model.minimax_*` | MiniMax `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`, `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.cursor_exec_path` | Cursor Agent executable path; default `cursor-agent` |
Expand Down
3 changes: 3 additions & 0 deletions scripts/eval_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--copilot_chat_optimizer_model", type=str)
p.add_argument("--copilot_chat_target_model", type=str)
p.add_argument("--copilot_chat_timeout", type=int)
p.add_argument("--minimax_region", type=str)
p.add_argument("--minimax_base_url", type=str)
p.add_argument("--minimax_api_key", type=str)
p.add_argument("--minimax_model", type=str)
Expand Down Expand Up @@ -307,6 +308,7 @@ def main() -> None:
"copilot_chat_optimizer_model": "model.copilot_chat_optimizer_model",
"copilot_chat_target_model": "model.copilot_chat_target_model",
"copilot_chat_timeout": "model.copilot_chat_timeout",
"minimax_region": "model.minimax_region",
"minimax_base_url": "model.minimax_base_url",
"minimax_api_key": "model.minimax_api_key",
"minimax_model": "model.minimax_model",
Expand Down Expand Up @@ -531,6 +533,7 @@ def _set_role(key: str, value: str) -> None:
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
)
configure_minimax_chat(
region=cfg.get("minimax_region") or None,
base_url=cfg.get("minimax_base_url") or None,
api_key=cfg.get("minimax_api_key") or None,
temperature=cfg.get("minimax_temperature"),
Expand Down
2 changes: 2 additions & 0 deletions scripts/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--target_qwen_chat_timeout_seconds", type=float)
p.add_argument("--target_qwen_chat_max_tokens", type=int)
p.add_argument("--target_qwen_chat_enable_thinking", type=_BOOL)
p.add_argument("--minimax_region", type=str)
p.add_argument("--minimax_base_url", type=str)
p.add_argument("--minimax_api_key", type=str)
p.add_argument("--minimax_model", type=str)
Expand Down Expand Up @@ -412,6 +413,7 @@ def _retired_option_sources(
"target_qwen_chat_timeout_seconds": "model.target_qwen_chat_timeout_seconds",
"target_qwen_chat_max_tokens": "model.target_qwen_chat_max_tokens",
"target_qwen_chat_enable_thinking": "model.target_qwen_chat_enable_thinking",
"minimax_region": "model.minimax_region",
"minimax_base_url": "model.minimax_base_url",
"minimax_api_key": "model.minimax_api_key",
"minimax_model": "model.minimax_model",
Expand Down
1 change: 1 addition & 0 deletions skillopt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"model.target_qwen_chat_timeout_seconds": "target_qwen_chat_timeout_seconds",
"model.target_qwen_chat_max_tokens": "target_qwen_chat_max_tokens",
"model.target_qwen_chat_enable_thinking": "target_qwen_chat_enable_thinking",
"model.minimax_region": "minimax_region",
"model.minimax_base_url": "minimax_base_url",
"model.minimax_api_key": "minimax_api_key",
"model.minimax_model": "minimax_model",
Expand Down
1 change: 1 addition & 0 deletions skillopt/engine/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ def _build_eval_env(split: str, env_num: int, seed: int):
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
)
configure_minimax_chat(
region=cfg.get("minimax_region") or None,
base_url=cfg.get("minimax_base_url") or None,
api_key=cfg.get("minimax_api_key") or None,
temperature=cfg.get("minimax_temperature"),
Expand Down
2 changes: 2 additions & 0 deletions skillopt/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ def configure_qwen_chat(

def configure_minimax_chat(
*,
region: str | None = None,
base_url: str | None = None,
api_key: str | None = None,
temperature: float | str | None = None,
Expand All @@ -693,6 +694,7 @@ def configure_minimax_chat(
enable_thinking: bool | str | None = None,
) -> None:
_minimax.configure_minimax_chat(
region=region,
base_url=base_url,
api_key=api_key,
temperature=temperature,
Expand Down
47 changes: 45 additions & 2 deletions skillopt/model/minimax_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,36 @@
default_model_for_backend,
)

BASE_URL = os.environ.get("MINIMAX_BASE_URL", "https://api.minimax.io/v1")
# The service is reachable through region-specific hostnames, so the selected
# region decides which OpenAI-compatible base URL the chat calls use. An
# explicit ``MINIMAX_BASE_URL`` still wins over the region default.
REGION_BASE_URLS = {
"global_en": "https://api.minimax.io/v1",
"cn_zh": "https://api.minimaxi.com/v1",
}
DEFAULT_REGION = "global_en"


def normalize_region(region: str | None) -> str:
"""Return a supported region key, defaulting to the global region."""
normalized = str(region or "").strip().lower().replace("-", "_")
if not normalized:
return DEFAULT_REGION
if normalized not in REGION_BASE_URLS:
raise ValueError(
f"Unsupported MiniMax region: {region!r}. "
f"Supported values are {sorted(REGION_BASE_URLS)}."
)
return normalized


def base_url_for_region(region: str | None) -> str:
"""Return the OpenAI-compatible base URL for a region."""
return REGION_BASE_URLS[normalize_region(region)]


REGION = normalize_region(os.environ.get("MINIMAX_REGION"))
BASE_URL = os.environ.get("MINIMAX_BASE_URL", "").strip() or base_url_for_region(REGION)
API_KEY = os.environ.get("MINIMAX_API_KEY", "")
TIMEOUT_SECONDS = float(os.environ.get("MINIMAX_TIMEOUT_SECONDS", "300") or 300)
MAX_TOKENS = int(os.environ.get("MINIMAX_MAX_TOKENS", "8000") or 8000)
Expand Down Expand Up @@ -177,15 +206,21 @@ def _chat_messages_impl(

def configure_minimax_chat(
*,
region: str | None = None,
base_url: str | None = None,
api_key: str | None = None,
temperature: float | str | None = None,
timeout_seconds: float | str | None = None,
max_tokens: int | str | None = None,
enable_thinking: bool | str | None = None,
) -> None:
global BASE_URL, API_KEY, TEMPERATURE, TIMEOUT_SECONDS, MAX_TOKENS, ENABLE_THINKING
global BASE_URL, API_KEY, TEMPERATURE, TIMEOUT_SECONDS, MAX_TOKENS, ENABLE_THINKING, REGION
with _config_lock:
if region is not None:
REGION = normalize_region(region)
os.environ["MINIMAX_REGION"] = REGION
BASE_URL = base_url_for_region(REGION)
os.environ["MINIMAX_BASE_URL"] = BASE_URL
if base_url is not None:
BASE_URL = str(base_url).strip() or BASE_URL
os.environ["MINIMAX_BASE_URL"] = BASE_URL
Expand Down Expand Up @@ -214,6 +249,14 @@ def get_max_tokens() -> int:
return MAX_TOKENS


def get_region() -> str:
return REGION


def get_base_url() -> str:
return BASE_URL


def chat_target(
system: str,
user: str,
Expand Down
118 changes: 118 additions & 0 deletions tests/test_minimax_region.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Tests for MiniMax service-region selection in the minimax_chat backend."""

from __future__ import annotations

import importlib
import os
from collections.abc import Iterator
from typing import Any

import pytest

from skillopt.model import minimax_backend

_GLOBAL_BASE_URL = "https://api.minimax.io/v1"
_CN_BASE_URL = "https://api.minimaxi.com/v1"
_ENV_KEYS = ("MINIMAX_REGION", "MINIMAX_BASE_URL")
_GLOBAL_KEYS = ("REGION", "BASE_URL")


@pytest.fixture(autouse=True)
def isolate_minimax_region() -> Iterator[None]:
env_snapshot = {key: os.environ.get(key) for key in _ENV_KEYS}
global_snapshot = {key: getattr(minimax_backend, key) for key in _GLOBAL_KEYS}
try:
yield
finally:
for key, value in global_snapshot.items():
setattr(minimax_backend, key, value)
for key, value in env_snapshot.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value


def _reload_with_env(monkeypatch: pytest.MonkeyPatch, **env: str | None) -> Any:
for key, value in env.items():
if value is None:
monkeypatch.delenv(key, raising=False)
else:
monkeypatch.setenv(key, value)
return importlib.reload(minimax_backend)


def test_supported_regions_expose_both_endpoints() -> None:
assert minimax_backend.REGION_BASE_URLS == {
"global_en": _GLOBAL_BASE_URL,
"cn_zh": _CN_BASE_URL,
}
assert minimax_backend.DEFAULT_REGION == "global_en"


def test_blank_region_falls_back_to_global() -> None:
assert minimax_backend.normalize_region(None) == "global_en"
assert minimax_backend.normalize_region(" ") == "global_en"
assert minimax_backend.base_url_for_region(None) == _GLOBAL_BASE_URL


def test_region_name_is_normalized() -> None:
assert minimax_backend.normalize_region("CN_ZH") == "cn_zh"
assert minimax_backend.normalize_region(" cn-zh ") == "cn_zh"
assert minimax_backend.base_url_for_region("cn-zh") == _CN_BASE_URL


def test_unsupported_region_is_rejected() -> None:
with pytest.raises(ValueError, match="Unsupported MiniMax region"):
minimax_backend.normalize_region("apac")


def test_region_env_selects_base_url_at_import(monkeypatch: pytest.MonkeyPatch) -> None:
module = _reload_with_env(monkeypatch, MINIMAX_REGION="cn_zh", MINIMAX_BASE_URL=None)
try:
assert module.REGION == "cn_zh"
assert module.BASE_URL == _CN_BASE_URL
finally:
monkeypatch.undo()
importlib.reload(minimax_backend)


def test_explicit_base_url_env_wins_over_region(monkeypatch: pytest.MonkeyPatch) -> None:
module = _reload_with_env(
monkeypatch,
MINIMAX_REGION="cn_zh",
MINIMAX_BASE_URL="https://proxy.internal/v1",
)
try:
assert module.REGION == "cn_zh"
assert module.BASE_URL == "https://proxy.internal/v1"
finally:
monkeypatch.undo()
importlib.reload(minimax_backend)


def test_configure_region_switches_base_url() -> None:
minimax_backend.configure_minimax_chat(region="cn_zh")
assert minimax_backend.get_region() == "cn_zh"
assert minimax_backend.get_base_url() == _CN_BASE_URL
assert minimax_backend._chat_url() == f"{_CN_BASE_URL}/chat/completions"

minimax_backend.configure_minimax_chat(region="global_en")
assert minimax_backend.get_region() == "global_en"
assert minimax_backend.get_base_url() == _GLOBAL_BASE_URL


def test_configure_base_url_overrides_region() -> None:
minimax_backend.configure_minimax_chat(
region="cn_zh",
base_url="https://proxy.internal/v1",
)
assert minimax_backend.get_region() == "cn_zh"
assert minimax_backend.get_base_url() == "https://proxy.internal/v1"


def test_configure_rejects_unsupported_region() -> None:
before = minimax_backend.get_base_url()
with pytest.raises(ValueError, match="Unsupported MiniMax region"):
minimax_backend.configure_minimax_chat(region="apac")
assert minimax_backend.get_base_url() == before