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
4 changes: 3 additions & 1 deletion skillopt/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ def chat_target(
retries=retries,
stage=stage,
reasoning_effort=reasoning_effort,
timeout=timeout,
)
if get_target_backend() == "openai_compatible":
return _openai_compat.chat_target(
Expand Down Expand Up @@ -326,7 +327,7 @@ def chat_optimizer_messages(
timeout=timeout,
)
if get_optimizer_backend() == "minimax_chat":
return _minimax.chat_target_messages(
return _minimax.chat_optimizer_messages(
messages=messages,
max_completion_tokens=max_completion_tokens,
retries=retries,
Expand Down Expand Up @@ -809,5 +810,6 @@ def set_optimizer_deployment(deployment: str) -> None:
_claude.set_optimizer_deployment(deployment)
_claude_code.set_optimizer_deployment(deployment)
_qwen.set_optimizer_deployment(deployment)
_minimax.set_optimizer_deployment(deployment)
_openai_compat.set_optimizer_deployment(deployment)
_codex.set_optimizer_deployment(deployment)
67 changes: 53 additions & 14 deletions skillopt/model/minimax_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""OpenAI-compatible MiniMax chat backend for the target path."""

from __future__ import annotations

import json
Expand Down Expand Up @@ -33,10 +34,7 @@ def normalize_region(region: str | None) -> str:
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)}."
)
raise ValueError(f"Unsupported MiniMax region: {region!r}. Supported values are {sorted(REGION_BASE_URLS)}.")
return normalized


Expand Down Expand Up @@ -69,6 +67,10 @@ def base_url_for_region(region: str | None) -> str:
"TARGET_DEPLOYMENT",
default_model_for_backend("minimax_chat"),
)
OPTIMIZER_DEPLOYMENT = os.environ.get(
"OPTIMIZER_DEPLOYMENT",
default_model_for_backend("minimax_chat"),
)

# Models whose thinking cannot actually be turned off. Per MiniMax's
# OpenAI-compatible docs the M2.x family accepts ``{"type": "disabled"}`` but
Expand Down Expand Up @@ -164,8 +166,11 @@ def _compat_message_from_payload(message: dict[str, Any], choice: dict[str, Any]
)


def _post_chat_completion(payload: dict[str, Any], timeout: float | None) -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
def _post_chat_completion(payload: dict[str, Any], timeout: float | None = None) -> dict[str, Any]:
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if API_KEY:
headers["Authorization"] = f"Bearer {API_KEY}"
req = urllib.request.Request(
Expand Down Expand Up @@ -205,9 +210,7 @@ def _chat_messages_impl(
"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(deployment or TARGET_DEPLOYMENT)}
if TEMPERATURE is not None:
payload["temperature"] = TEMPERATURE
if tools:
Expand All @@ -234,7 +237,7 @@ def _chat_messages_impl(
return text, usage_info
except Exception as e: # noqa: BLE001
last_err = e
time.sleep(min(2 ** attempt, 30))
time.sleep(min(2**attempt, 30))
raise RuntimeError(f"MiniMax chat call failed after {retries} retries: {last_err}")


Expand Down Expand Up @@ -304,14 +307,15 @@ def chat_target(
stage: str = "target",
reasoning_effort: str | None = None,
timeout: float | None = None,
) -> tuple[str, dict[int]]:
) -> 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,
timeout=timeout,
)

Expand All @@ -324,20 +328,22 @@ def chat_optimizer(
stage: str = "optimizer",
reasoning_effort: str | None = None,
timeout: float | None = None,
) -> tuple[str, dict[int]]:
) -> tuple[str, dict[str, int]]:
"""Optimizer chat call. Backend stores the trained skill; uses the same
MiniMax-proxied OpenAI-compat endpoint as `chat_target`. Added in the
parallel-training fix; previously missing in skillopt 0.2.0's
miniamax backend, which forced the dispatcher into _openai.chat_optimizer
minimax backend, which forced the dispatcher into _openai.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,
timeout=timeout,
)

Expand All @@ -363,6 +369,33 @@ def chat_target_messages(
tools=tools,
tool_choice=tool_choice,
return_message=return_message,
deployment=TARGET_DEPLOYMENT,
timeout=timeout,
)


def chat_optimizer_messages(
messages: list[dict[str, Any]],
max_completion_tokens: int = 16384,
retries: int = 5,
stage: str = "optimizer",
reasoning_effort: str | None = None,
*,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | dict[str, Any] | None = None,
return_message: bool = False,
timeout: float | None = None,
) -> tuple[Any, dict[str, int]]:
del reasoning_effort
return _chat_messages_impl(
messages,
max_completion_tokens,
retries,
stage,
tools=tools,
tool_choice=tool_choice,
return_message=return_message,
deployment=OPTIMIZER_DEPLOYMENT,
timeout=timeout,
)

Expand All @@ -382,4 +415,10 @@ def set_reasoning_effort(effort: str | None) -> None:
def set_target_deployment(deployment: str) -> None:
global TARGET_DEPLOYMENT
TARGET_DEPLOYMENT = deployment or default_model_for_backend("minimax_chat")
os.environ["TARGET_DEPLOYMENT"] = TARGET_DEPLOYMENT
os.environ["TARGET_DEPLOYMENT"] = TARGET_DEPLOYMENT


def set_optimizer_deployment(deployment: str) -> None:
global OPTIMIZER_DEPLOYMENT
OPTIMIZER_DEPLOYMENT = deployment or default_model_for_backend("minimax_chat")
os.environ["OPTIMIZER_DEPLOYMENT"] = OPTIMIZER_DEPLOYMENT
145 changes: 127 additions & 18 deletions tests/test_minimax_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

import importlib
import importlib.util
import json
import os
import sys
import types
from collections.abc import Iterator
Expand Down Expand Up @@ -40,9 +42,7 @@ def __call__(self, request: Any, timeout: float | None = None) -> _FakeResponse:
)
return _FakeResponse(
{
"choices": [
{"message": {"content": self.content}, "finish_reason": "stop"}
],
"choices": [{"message": {"content": self.content}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 2, "completion_tokens": 1, "total_tokens": 3},
}
)
Expand Down Expand Up @@ -71,6 +71,7 @@ def minimax_backend() -> Iterator[Any]:
snapshot = {
"ENABLE_THINKING": backend.ENABLE_THINKING,
"TARGET_DEPLOYMENT": backend.TARGET_DEPLOYMENT,
"OPTIMIZER_DEPLOYMENT": backend.OPTIMIZER_DEPLOYMENT,
"API_KEY": backend.API_KEY,
"BASE_URL": backend.BASE_URL,
}
Expand All @@ -91,11 +92,11 @@ def test_default_deployment_is_current_model(minimax_backend: Any) -> None:
from skillopt.model.common import default_model_for_backend

assert default_model_for_backend("minimax_chat") == "MiniMax-M3"
assert minimax_backend.TARGET_DEPLOYMENT == "MiniMax-M3"
assert minimax_backend.OPTIMIZER_DEPLOYMENT == "MiniMax-M3"


def test_always_on_model_sends_adaptive_not_disabled(
monkeypatch: pytest.MonkeyPatch, minimax_backend: Any
) -> None:
def test_always_on_model_sends_adaptive_not_disabled(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
"""M2.x cannot turn thinking off, so never claim it is disabled.

MiniMax documents that the M2 family accepts ``{"type": "disabled"}`` but
Expand All @@ -113,9 +114,7 @@ def test_always_on_model_sends_adaptive_not_disabled(
assert payload["thinking"] == {"type": "adaptive"}


def test_adaptive_model_respects_disabled_flag(
monkeypatch: pytest.MonkeyPatch, minimax_backend: Any
) -> None:
def test_adaptive_model_respects_disabled_flag(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
minimax_backend.ENABLE_THINKING = False
minimax_backend.TARGET_DEPLOYMENT = "MiniMax-M3"
recorder = _record_urlopen(monkeypatch, minimax_backend)
Expand All @@ -127,9 +126,7 @@ def test_adaptive_model_respects_disabled_flag(
assert payload["thinking"] == {"type": "disabled"}


def test_adaptive_model_respects_enabled_flag(
monkeypatch: pytest.MonkeyPatch, minimax_backend: Any
) -> None:
def test_adaptive_model_respects_enabled_flag(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
minimax_backend.ENABLE_THINKING = True
minimax_backend.TARGET_DEPLOYMENT = "MiniMax-M3"
recorder = _record_urlopen(monkeypatch, minimax_backend)
Expand All @@ -139,9 +136,7 @@ def test_adaptive_model_respects_enabled_flag(
assert recorder.calls[0]["payload"]["thinking"] == {"type": "adaptive"}


def test_unsupported_chat_template_kwargs_is_never_sent(
monkeypatch: pytest.MonkeyPatch, minimax_backend: Any
) -> None:
def test_unsupported_chat_template_kwargs_is_never_sent(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
"""Guards the original regression.

``chat_template_kwargs.enable_thinking`` is a Qwen/HuggingFace-serving
Expand All @@ -158,9 +153,7 @@ def test_unsupported_chat_template_kwargs_is_never_sent(
assert "chat_template_kwargs" not in recorder.calls[0]["payload"]


def test_unknown_deployment_defaults_to_adaptive(
monkeypatch: pytest.MonkeyPatch, minimax_backend: Any
) -> None:
def test_unknown_deployment_defaults_to_adaptive(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
"""An unrecognized model follows the documented API default (thinking on)."""
minimax_backend.ENABLE_THINKING = True
minimax_backend.TARGET_DEPLOYMENT = "MiniMax-Future-9"
Expand All @@ -169,3 +162,119 @@ def test_unknown_deployment_defaults_to_adaptive(
minimax_backend.chat_target("system", "user", retries=1)

assert recorder.calls[0]["payload"]["thinking"] == {"type": "adaptive"}


def test_chat_optimizer_and_target_use_respective_deployments(
monkeypatch: pytest.MonkeyPatch, minimax_backend: Any
) -> None:
minimax_backend.TARGET_DEPLOYMENT = "MiniMax-Target-Model"
minimax_backend.OPTIMIZER_DEPLOYMENT = "MiniMax-Optimizer-Model"
recorder = _record_urlopen(monkeypatch, minimax_backend)

minimax_backend.chat_target("sys_target", "user_target", retries=1)
minimax_backend.chat_optimizer("sys_opt", "user_opt", retries=1)
minimax_backend.chat_target_messages([{"role": "user", "content": "msg_target"}], retries=1)
minimax_backend.chat_optimizer_messages([{"role": "user", "content": "msg_opt"}], retries=1)

assert recorder.calls[0]["payload"]["model"] == "MiniMax-Target-Model"
assert recorder.calls[1]["payload"]["model"] == "MiniMax-Optimizer-Model"
assert recorder.calls[2]["payload"]["model"] == "MiniMax-Target-Model"
assert recorder.calls[3]["payload"]["model"] == "MiniMax-Optimizer-Model"


def test_set_optimizer_and_target_deployment(minimax_backend: Any) -> None:
minimax_backend.set_target_deployment("MiniMax-New-Target")
assert minimax_backend.TARGET_DEPLOYMENT == "MiniMax-New-Target"
assert os.environ.get("TARGET_DEPLOYMENT") == "MiniMax-New-Target"

minimax_backend.set_optimizer_deployment("MiniMax-New-Optimizer")
assert minimax_backend.OPTIMIZER_DEPLOYMENT == "MiniMax-New-Optimizer"
assert os.environ.get("OPTIMIZER_DEPLOYMENT") == "MiniMax-New-Optimizer"


def test_timeout_forwarded_to_urlopen(monkeypatch: pytest.MonkeyPatch, minimax_backend: Any) -> None:
recorder = _record_urlopen(monkeypatch, minimax_backend)

minimax_backend.chat_target("system", "user", retries=1, timeout=42.5)
minimax_backend.chat_optimizer("system", "user", retries=1, timeout=55.0)
minimax_backend.chat_target_messages([{"role": "user", "content": "hi"}], retries=1, timeout=60.0)
minimax_backend.chat_optimizer_messages([{"role": "user", "content": "hi"}], retries=1, timeout=75.0)

assert recorder.calls[0]["timeout"] == 42.5
assert recorder.calls[1]["timeout"] == 55.0
assert recorder.calls[2]["timeout"] == 60.0
assert recorder.calls[3]["timeout"] == 75.0


def test_fresh_import_optimizer_calls_without_setter(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression: calling chat_optimizer or chat_optimizer_messages without calling

set_optimizer_deployment() on a fresh import must not raise NameError for
OPTIMIZER_DEPLOYMENT.
"""
_install_openai_stub()
monkeypatch.delenv("OPTIMIZER_DEPLOYMENT", raising=False)
monkeypatch.delenv("TARGET_DEPLOYMENT", raising=False)

from skillopt.model import minimax_backend as backend

module = importlib.reload(backend)
recorder = _record_urlopen(monkeypatch, module)

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

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"


def test_fresh_import_respects_optimizer_deployment_env(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_install_openai_stub()
monkeypatch.setenv("OPTIMIZER_DEPLOYMENT", "MiniMax-Env-Optimizer")
monkeypatch.setenv("TARGET_DEPLOYMENT", "MiniMax-Env-Target")

from skillopt.model import minimax_backend as backend

module = importlib.reload(backend)
recorder = _record_urlopen(monkeypatch, module)

module.chat_optimizer("system prompt", "user query", retries=1)
module.chat_optimizer_messages([{"role": "user", "content": "user query"}], retries=1)
module.chat_target("system prompt", "user query", retries=1)
module.chat_target_messages([{"role": "user", "content": "user query"}], retries=1)

assert recorder.calls[0]["payload"]["model"] == "MiniMax-Env-Optimizer"
assert recorder.calls[1]["payload"]["model"] == "MiniMax-Env-Optimizer"
assert recorder.calls[2]["payload"]["model"] == "MiniMax-Env-Target"
assert recorder.calls[3]["payload"]["model"] == "MiniMax-Env-Target"


def test_model_dispatcher_chat_optimizer_messages_minimax(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_install_openai_stub()
import skillopt.model as model
from skillopt.model import backend_config
from skillopt.model import minimax_backend as backend

module = importlib.reload(backend)
recorder = _record_urlopen(monkeypatch, module)

backend_config.set_optimizer_backend("minimax_chat")
model.set_optimizer_deployment("MiniMax-Custom-Opt")

res, _ = model.chat_optimizer("system", "user", retries=1, timeout=99)
assert res == "answer"
assert recorder.calls[0]["payload"]["model"] == "MiniMax-Custom-Opt"
assert recorder.calls[0]["timeout"] == 99

res_msg, _ = model.chat_optimizer_messages([{"role": "user", "content": "test"}], retries=1, timeout=88)
assert res_msg == "answer"
assert recorder.calls[1]["payload"]["model"] == "MiniMax-Custom-Opt"
assert recorder.calls[1]["timeout"] == 88