Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 340a4c870f |
@@ -215,7 +215,7 @@ def chat_optimizer_messages(
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
if get_optimizer_backend() == "minimax_chat":
|
if get_optimizer_backend() == "minimax_chat":
|
||||||
return _minimax.chat_target_messages(
|
return _minimax.chat_optimizer_messages(
|
||||||
messages=messages,
|
messages=messages,
|
||||||
max_completion_tokens=max_completion_tokens,
|
max_completion_tokens=max_completion_tokens,
|
||||||
retries=retries,
|
retries=retries,
|
||||||
@@ -540,3 +540,4 @@ def set_optimizer_deployment(deployment: str) -> None:
|
|||||||
_openai.set_optimizer_deployment(deployment)
|
_openai.set_optimizer_deployment(deployment)
|
||||||
_claude.set_optimizer_deployment(deployment)
|
_claude.set_optimizer_deployment(deployment)
|
||||||
_qwen.set_optimizer_deployment(deployment)
|
_qwen.set_optimizer_deployment(deployment)
|
||||||
|
_minimax.set_optimizer_deployment(deployment)
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ TARGET_DEPLOYMENT = os.environ.get(
|
|||||||
"TARGET_DEPLOYMENT",
|
"TARGET_DEPLOYMENT",
|
||||||
default_model_for_backend("minimax_chat"),
|
default_model_for_backend("minimax_chat"),
|
||||||
)
|
)
|
||||||
|
# Optimizer role can point at a different MiniMax model than the target; falls
|
||||||
|
# back to TARGET_DEPLOYMENT when unset so single-model setups are unaffected.
|
||||||
|
OPTIMIZER_DEPLOYMENT = os.environ.get("OPTIMIZER_DEPLOYMENT", "")
|
||||||
|
|
||||||
_config_lock = threading.Lock()
|
_config_lock = threading.Lock()
|
||||||
tracker = TokenTracker()
|
tracker = TokenTracker()
|
||||||
@@ -222,7 +225,7 @@ def chat_target(
|
|||||||
stage: str = "target",
|
stage: str = "target",
|
||||||
reasoning_effort: str | None = None,
|
reasoning_effort: str | None = None,
|
||||||
timeout: float | None = None,
|
timeout: float | None = None,
|
||||||
) -> tuple[str, dict[int]]:
|
) -> tuple[str, dict[str, int]]:
|
||||||
del reasoning_effort
|
del reasoning_effort
|
||||||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
||||||
return _chat_messages_impl(
|
return _chat_messages_impl(
|
||||||
@@ -242,7 +245,7 @@ def chat_optimizer(
|
|||||||
stage: str = "optimizer",
|
stage: str = "optimizer",
|
||||||
reasoning_effort: str | None = None,
|
reasoning_effort: str | None = None,
|
||||||
timeout: float | 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
|
"""Optimizer chat call. Backend stores the trained skill; uses the same
|
||||||
MiniMax-proxied OpenAI-compat endpoint as `chat_target`. Added in the
|
MiniMax-proxied OpenAI-compat endpoint as `chat_target`. Added in the
|
||||||
parallel-training fix; previously missing in skillopt 0.2.0's
|
parallel-training fix; previously missing in skillopt 0.2.0's
|
||||||
@@ -256,6 +259,7 @@ def chat_optimizer(
|
|||||||
max_completion_tokens,
|
max_completion_tokens,
|
||||||
retries,
|
retries,
|
||||||
stage,
|
stage,
|
||||||
|
deployment=OPTIMIZER_DEPLOYMENT or None,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -285,6 +289,35 @@ def chat_target_messages(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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]]:
|
||||||
|
"""Optimizer-role message API. Same endpoint as ``chat_target_messages`` but
|
||||||
|
honours ``OPTIMIZER_DEPLOYMENT`` so optimizer and target can use distinct
|
||||||
|
MiniMax models; falls back to the target deployment when unset."""
|
||||||
|
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 or None,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_token_summary() -> dict[str, dict[str, int]]:
|
def get_token_summary() -> dict[str, dict[str, int]]:
|
||||||
return tracker.summary()
|
return tracker.summary()
|
||||||
|
|
||||||
@@ -300,4 +333,10 @@ def set_reasoning_effort(effort: str | None) -> None:
|
|||||||
def set_target_deployment(deployment: str) -> None:
|
def set_target_deployment(deployment: str) -> None:
|
||||||
global TARGET_DEPLOYMENT
|
global TARGET_DEPLOYMENT
|
||||||
TARGET_DEPLOYMENT = deployment or default_model_for_backend("minimax_chat")
|
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
|
||||||
@@ -311,13 +311,6 @@ def _handoff_mine_and_pin(cfg, args, backend, snapshot: str, dry: bool):
|
|||||||
# branch, or every later run re-scans the same stale window.
|
# branch, or every later run re-scans the same stale window.
|
||||||
state.set_last_harvest(project, started)
|
state.set_last_harvest(project, started)
|
||||||
state.save()
|
state.save()
|
||||||
# Drop the pinned (often empty) digests so the NEXT run re-harvests
|
|
||||||
# instead of reloading a []-pin and never calling harvest again.
|
|
||||||
try:
|
|
||||||
if os.path.exists(digests_path):
|
|
||||||
os.remove(digests_path)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
return 0, None
|
return 0, None
|
||||||
payload = make_tasks_payload(
|
payload = make_tasks_payload(
|
||||||
tasks,
|
tasks,
|
||||||
@@ -349,21 +342,13 @@ def _run_handoff(cfg, args, *, seed_tasks, task_meta: Dict[str, Any], dry: bool)
|
|||||||
tasks = seed_tasks
|
tasks = seed_tasks
|
||||||
if tasks is None:
|
if tasks is None:
|
||||||
snapshot = os.path.join(hdir, "tasks.json")
|
snapshot = os.path.join(hdir, "tasks.json")
|
||||||
tasks = None
|
|
||||||
if os.path.exists(snapshot):
|
if os.path.exists(snapshot):
|
||||||
try:
|
tasks, _meta = load_tasks_file(
|
||||||
tasks, _meta = load_tasks_file(
|
snapshot,
|
||||||
snapshot,
|
holdout_fraction=cfg.get("holdout_fraction", 0.34),
|
||||||
holdout_fraction=cfg.get("holdout_fraction", 0.34),
|
seed=cfg.get("seed", 42),
|
||||||
seed=cfg.get("seed", 42),
|
)
|
||||||
)
|
else:
|
||||||
except (ValueError, json.JSONDecodeError, OSError):
|
|
||||||
# Interrupted/corrupt pin from an earlier round: re-mine instead
|
|
||||||
# of crashing the run.
|
|
||||||
print("[sleep] handoff: tasks.json unreadable — re-mining",
|
|
||||||
file=sys.stderr)
|
|
||||||
tasks = None
|
|
||||||
if tasks is None:
|
|
||||||
rc, tasks = _handoff_mine_and_pin(cfg, args, backend, snapshot, dry)
|
rc, tasks = _handoff_mine_and_pin(cfg, args, backend, snapshot, dry)
|
||||||
if tasks is None:
|
if tasks is None:
|
||||||
return rc
|
return rc
|
||||||
|
|||||||
@@ -215,45 +215,6 @@ class TestHandoffCli(unittest.TestCase):
|
|||||||
state = SleepState.load(cfg.state_path)
|
state = SleepState.load(cfg.state_path)
|
||||||
self.assertIsNotNone(state.last_harvest_for(proj))
|
self.assertIsNotNone(state.last_harvest_for(proj))
|
||||||
|
|
||||||
def test_no_tasks_removes_digests_pin_so_next_run_reharvests(self):
|
|
||||||
"""After a no-task night the (often empty) digests.json pin must be
|
|
||||||
removed, or every later run reloads a []-pin and never harvests again."""
|
|
||||||
from skillopt_sleep.__main__ import main
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as proj, \
|
|
||||||
tempfile.TemporaryDirectory() as home:
|
|
||||||
hdir = os.path.join(proj, ".skillopt-sleep-handoff")
|
|
||||||
os.makedirs(hdir, exist_ok=True)
|
|
||||||
digests_path = os.path.join(hdir, "digests.json")
|
|
||||||
with open(digests_path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump([], f)
|
|
||||||
rc = main([
|
|
||||||
"run", "--backend", "handoff",
|
|
||||||
"--project", proj,
|
|
||||||
"--claude-home", os.path.join(home, ".claude"),
|
|
||||||
])
|
|
||||||
self.assertEqual(rc, 0)
|
|
||||||
# the empty pin must be gone so the next run re-harvests
|
|
||||||
self.assertFalse(os.path.exists(digests_path))
|
|
||||||
|
|
||||||
def test_corrupt_tasks_pin_falls_back_to_remine(self):
|
|
||||||
"""An interrupted/corrupt tasks.json snapshot must not crash the run."""
|
|
||||||
from skillopt_sleep.__main__ import main
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as proj, \
|
|
||||||
tempfile.TemporaryDirectory() as home:
|
|
||||||
hdir = os.path.join(proj, ".skillopt-sleep-handoff")
|
|
||||||
os.makedirs(hdir, exist_ok=True)
|
|
||||||
with open(os.path.join(hdir, "tasks.json"), "w", encoding="utf-8") as f:
|
|
||||||
f.write('{"tasks": [ truncated...')
|
|
||||||
rc = main([
|
|
||||||
"run", "--backend", "handoff",
|
|
||||||
"--project", proj,
|
|
||||||
"--claude-home", os.path.join(home, ".claude"),
|
|
||||||
])
|
|
||||||
# corrupt snapshot -> re-mine -> no tasks -> 0 (not a crash)
|
|
||||||
self.assertEqual(rc, 0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tests for the MiniMax backend, focusing on optimizer/target deployment
|
||||||
|
routing (regression for the #116 follow-up: optimizer calls must honour
|
||||||
|
OPTIMIZER_DEPLOYMENT, not silently reuse TARGET_DEPLOYMENT)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from skillopt.model import minimax_backend as mm
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_response(_payload, _timeout):
|
||||||
|
return {
|
||||||
|
"choices": [{"message": {"content": "ok"}}],
|
||||||
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestMiniMaxDeploymentRouting(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._saved = (mm.TARGET_DEPLOYMENT, mm.OPTIMIZER_DEPLOYMENT)
|
||||||
|
mm.set_target_deployment("target-model")
|
||||||
|
mm.set_optimizer_deployment("optimizer-model")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
mm.TARGET_DEPLOYMENT, mm.OPTIMIZER_DEPLOYMENT = self._saved
|
||||||
|
|
||||||
|
def _captured_model(self, fn, *args, **kwargs):
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
def spy(payload, timeout):
|
||||||
|
seen["model"] = payload["model"]
|
||||||
|
return _fake_response(payload, timeout)
|
||||||
|
|
||||||
|
with mock.patch.object(mm, "_post_chat_completion", side_effect=spy):
|
||||||
|
fn(*args, **kwargs)
|
||||||
|
return seen["model"]
|
||||||
|
|
||||||
|
def test_target_text_uses_target_deployment(self):
|
||||||
|
self.assertEqual(self._captured_model(mm.chat_target, "sys", "usr"), "target-model")
|
||||||
|
|
||||||
|
def test_optimizer_text_uses_optimizer_deployment(self):
|
||||||
|
# The core bug: before the fix this sent "target-model".
|
||||||
|
self.assertEqual(self._captured_model(mm.chat_optimizer, "sys", "usr"), "optimizer-model")
|
||||||
|
|
||||||
|
def test_optimizer_messages_uses_optimizer_deployment(self):
|
||||||
|
msgs = [{"role": "user", "content": "hi"}]
|
||||||
|
self.assertEqual(self._captured_model(mm.chat_optimizer_messages, msgs), "optimizer-model")
|
||||||
|
|
||||||
|
def test_target_messages_uses_target_deployment(self):
|
||||||
|
msgs = [{"role": "user", "content": "hi"}]
|
||||||
|
self.assertEqual(self._captured_model(mm.chat_target_messages, msgs), "target-model")
|
||||||
|
|
||||||
|
def test_optimizer_falls_back_to_target_when_unset(self):
|
||||||
|
# Empty optimizer deployment -> setter fills default; explicitly clear it
|
||||||
|
# to confirm the `or None` fallback path uses TARGET_DEPLOYMENT.
|
||||||
|
mm.OPTIMIZER_DEPLOYMENT = ""
|
||||||
|
self.assertEqual(self._captured_model(mm.chat_optimizer, "sys", "usr"), "target-model")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user