From 181ec0a3805d9abeafc0a95d8492f050268224c2 Mon Sep 17 00:00:00 2001 From: Rohith Pariki Date: Wed, 26 Aug 2026 03:50:50 +0530 Subject: [PATCH 1/2] fix(model): complete MiniMax optimizer role deployment, timeout forwarding, and typing --- skillopt/model/__init__.py | 4 +- skillopt/model/minimax_backend.py | 51 ++++++++++++++++++++--- tests/test_minimax_backend.py | 69 +++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/skillopt/model/__init__.py b/skillopt/model/__init__.py index 9033bc98..b732ab8c 100644 --- a/skillopt/model/__init__.py +++ b/skillopt/model/__init__.py @@ -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( @@ -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, @@ -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) diff --git a/skillopt/model/minimax_backend.py b/skillopt/model/minimax_backend.py index 9736a533..e60c0598 100644 --- a/skillopt/model/minimax_backend.py +++ b/skillopt/model/minimax_backend.py @@ -164,8 +164,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( @@ -304,7 +307,7 @@ 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( @@ -312,6 +315,7 @@ def chat_target( max_completion_tokens, retries, stage, + deployment=TARGET_DEPLOYMENT, timeout=timeout, ) @@ -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, ) @@ -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, ) @@ -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 \ No newline at end of file + 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 diff --git a/tests/test_minimax_backend.py b/tests/test_minimax_backend.py index ec049a21..076b02c2 100644 --- a/tests/test_minimax_backend.py +++ b/tests/test_minimax_backend.py @@ -169,3 +169,72 @@ 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_optimizer_deployment_and_timeout_forwarding( + monkeypatch: pytest.MonkeyPatch, minimax_backend: Any +) -> None: + minimax_backend.set_optimizer_deployment("MiniMax-Optimizer-Custom") + minimax_backend.set_target_deployment("MiniMax-Target-Custom") + recorder = _record_urlopen(monkeypatch, minimax_backend) + + # chat_optimizer uses optimizer deployment + minimax_backend.chat_optimizer("sys_opt", "user_opt", retries=1, timeout=45.0) + assert recorder.calls[0]["payload"]["model"] == "MiniMax-Optimizer-Custom" + assert recorder.calls[0]["timeout"] == 45.0 + + # chat_target uses target deployment + minimax_backend.chat_target("sys_tgt", "user_tgt", retries=1, timeout=60.0) + assert recorder.calls[1]["payload"]["model"] == "MiniMax-Target-Custom" + assert recorder.calls[1]["timeout"] == 60.0 + + +def test_chat_optimizer_messages_dispatches_with_tools( + monkeypatch: pytest.MonkeyPatch, minimax_backend: Any +) -> None: + minimax_backend.set_optimizer_deployment("MiniMax-Opt-Tools") + recorder = _record_urlopen(monkeypatch, minimax_backend) + + messages = [{"role": "user", "content": "analyze"}] + tools = [{"type": "function", "function": {"name": "test_tool"}}] + + minimax_backend.chat_optimizer_messages( + messages=messages, + retries=1, + tools=tools, + tool_choice="auto", + timeout=30.0, + ) + + assert recorder.calls[0]["payload"]["model"] == "MiniMax-Opt-Tools" + assert recorder.calls[0]["payload"]["tools"] == tools + assert recorder.calls[0]["payload"]["tool_choice"] == "auto" + assert recorder.calls[0]["timeout"] == 30.0 + + +def test_model_module_level_minimax_setters_and_timeout(monkeypatch: pytest.MonkeyPatch) -> None: + import skillopt.model as model + from skillopt.model import minimax_backend as backend + + recorder = _record_urlopen(monkeypatch, backend) + + model.set_target_backend("minimax_chat") + model.set_optimizer_backend("minimax_chat") + model.set_target_deployment("MiniMax-Global-Target") + model.set_optimizer_deployment("MiniMax-Global-Optimizer") + + assert backend.TARGET_DEPLOYMENT == "MiniMax-Global-Target" + assert backend.OPTIMIZER_DEPLOYMENT == "MiniMax-Global-Optimizer" + + model.chat_target("sys", "user", retries=1, timeout=99.0) + assert recorder.calls[0]["payload"]["model"] == "MiniMax-Global-Target" + assert recorder.calls[0]["timeout"] == 99.0 + + model.chat_optimizer_messages( + messages=[{"role": "user", "content": "hi"}], + retries=1, + timeout=12.0, + ) + assert recorder.calls[1]["payload"]["model"] == "MiniMax-Global-Optimizer" + assert recorder.calls[1]["timeout"] == 12.0 + From 1e941d631225911aa0d4767ba339eda72fb1d25b Mon Sep 17 00:00:00 2001 From: Rohith Pariki Date: Mon, 31 Aug 2026 13:55:39 +0530 Subject: [PATCH 2/2] fix(model): initialize MiniMax OPTIMIZER_DEPLOYMENT at import and add fresh-import regression --- skillopt/model/minimax_backend.py | 16 +-- tests/test_minimax_backend.py | 170 ++++++++++++++++++------------ 2 files changed, 113 insertions(+), 73 deletions(-) diff --git a/skillopt/model/minimax_backend.py b/skillopt/model/minimax_backend.py index e60c0598..04bae635 100644 --- a/skillopt/model/minimax_backend.py +++ b/skillopt/model/minimax_backend.py @@ -1,4 +1,5 @@ """OpenAI-compatible MiniMax chat backend for the target path.""" + from __future__ import annotations import json @@ -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 @@ -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 @@ -208,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: @@ -237,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}") diff --git a/tests/test_minimax_backend.py b/tests/test_minimax_backend.py index 076b02c2..d8a9e650 100644 --- a/tests/test_minimax_backend.py +++ b/tests/test_minimax_backend.py @@ -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 @@ -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}, } ) @@ -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, } @@ -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 @@ -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) @@ -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) @@ -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 @@ -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" @@ -171,70 +164,117 @@ def test_unknown_deployment_defaults_to_adaptive( assert recorder.calls[0]["payload"]["thinking"] == {"type": "adaptive"} -def test_optimizer_deployment_and_timeout_forwarding( +def test_chat_optimizer_and_target_use_respective_deployments( monkeypatch: pytest.MonkeyPatch, minimax_backend: Any ) -> None: - minimax_backend.set_optimizer_deployment("MiniMax-Optimizer-Custom") - minimax_backend.set_target_deployment("MiniMax-Target-Custom") + minimax_backend.TARGET_DEPLOYMENT = "MiniMax-Target-Model" + minimax_backend.OPTIMIZER_DEPLOYMENT = "MiniMax-Optimizer-Model" recorder = _record_urlopen(monkeypatch, minimax_backend) - # chat_optimizer uses optimizer deployment - minimax_backend.chat_optimizer("sys_opt", "user_opt", retries=1, timeout=45.0) - assert recorder.calls[0]["payload"]["model"] == "MiniMax-Optimizer-Custom" - assert recorder.calls[0]["timeout"] == 45.0 + 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) - # chat_target uses target deployment - minimax_backend.chat_target("sys_tgt", "user_tgt", retries=1, timeout=60.0) - assert recorder.calls[1]["payload"]["model"] == "MiniMax-Target-Custom" - assert recorder.calls[1]["timeout"] == 60.0 + 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_chat_optimizer_messages_dispatches_with_tools( - monkeypatch: pytest.MonkeyPatch, minimax_backend: Any -) -> None: - minimax_backend.set_optimizer_deployment("MiniMax-Opt-Tools") +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) - messages = [{"role": "user", "content": "analyze"}] - tools = [{"type": "function", "function": {"name": "test_tool"}}] + 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) - minimax_backend.chat_optimizer_messages( - messages=messages, - retries=1, - tools=tools, - tool_choice="auto", - timeout=30.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 - assert recorder.calls[0]["payload"]["model"] == "MiniMax-Opt-Tools" - assert recorder.calls[0]["payload"]["tools"] == tools - assert recorder.calls[0]["payload"]["tool_choice"] == "auto" - assert recorder.calls[0]["timeout"] == 30.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") -def test_model_module_level_minimax_setters_and_timeout(monkeypatch: pytest.MonkeyPatch) -> None: - import skillopt.model as model from skillopt.model import minimax_backend as backend - recorder = _record_urlopen(monkeypatch, 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" + - model.set_target_backend("minimax_chat") - model.set_optimizer_backend("minimax_chat") - model.set_target_deployment("MiniMax-Global-Target") - model.set_optimizer_deployment("MiniMax-Global-Optimizer") +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 - assert backend.TARGET_DEPLOYMENT == "MiniMax-Global-Target" - assert backend.OPTIMIZER_DEPLOYMENT == "MiniMax-Global-Optimizer" + module = importlib.reload(backend) + recorder = _record_urlopen(monkeypatch, module) - model.chat_target("sys", "user", retries=1, timeout=99.0) - assert recorder.calls[0]["payload"]["model"] == "MiniMax-Global-Target" - assert recorder.calls[0]["timeout"] == 99.0 + backend_config.set_optimizer_backend("minimax_chat") + model.set_optimizer_deployment("MiniMax-Custom-Opt") - model.chat_optimizer_messages( - messages=[{"role": "user", "content": "hi"}], - retries=1, - timeout=12.0, - ) - assert recorder.calls[1]["payload"]["model"] == "MiniMax-Global-Optimizer" - assert recorder.calls[1]["timeout"] == 12.0 + 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