test(sleep): add verifier-discipline stress test for the validation gate (#87)
Adds a reward-hacking stress test ensuring the consolidation gate rejects skill edits that game train/replay tasks while degrading held-out behavior. Also wires the minimax_chat backend into scripts/eval_only.py (coexisting with the qwen wiring from #85). Closes #67.
This commit is contained in:
+36
-1
@@ -29,6 +29,7 @@ from skillopt.model import (
|
|||||||
configure_claude_code_exec,
|
configure_claude_code_exec,
|
||||||
configure_codex_exec,
|
configure_codex_exec,
|
||||||
configure_qwen_chat,
|
configure_qwen_chat,
|
||||||
|
configure_minimax_chat,
|
||||||
set_reasoning_effort,
|
set_reasoning_effort,
|
||||||
set_target_backend,
|
set_target_backend,
|
||||||
set_target_deployment,
|
set_target_deployment,
|
||||||
@@ -138,7 +139,7 @@ def parse_args() -> argparse.Namespace:
|
|||||||
# Legacy flat overrides
|
# Legacy flat overrides
|
||||||
p.add_argument("--env", type=str)
|
p.add_argument("--env", type=str)
|
||||||
p.add_argument("--backend", type=str,
|
p.add_argument("--backend", type=str,
|
||||||
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec"])
|
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec", "minimax", "minimax_chat"])
|
||||||
p.add_argument("--optimizer_model", type=str)
|
p.add_argument("--optimizer_model", type=str)
|
||||||
p.add_argument("--target_model", type=str)
|
p.add_argument("--target_model", type=str)
|
||||||
p.add_argument("--optimizer_backend", type=str)
|
p.add_argument("--optimizer_backend", type=str)
|
||||||
@@ -180,6 +181,12 @@ def parse_args() -> argparse.Namespace:
|
|||||||
p.add_argument("--claude_code_exec_use_sdk", type=str)
|
p.add_argument("--claude_code_exec_use_sdk", type=str)
|
||||||
p.add_argument("--claude_code_exec_effort", type=str)
|
p.add_argument("--claude_code_exec_effort", type=str)
|
||||||
p.add_argument("--claude_code_exec_max_thinking_tokens", type=int)
|
p.add_argument("--claude_code_exec_max_thinking_tokens", type=int)
|
||||||
|
p.add_argument("--minimax_base_url", type=str)
|
||||||
|
p.add_argument("--minimax_api_key", type=str)
|
||||||
|
p.add_argument("--minimax_model", type=str)
|
||||||
|
p.add_argument("--minimax_temperature", type=float)
|
||||||
|
p.add_argument("--minimax_max_tokens", type=int)
|
||||||
|
p.add_argument("--minimax_enable_thinking", type=_BOOL)
|
||||||
p.add_argument("--out_root", type=str)
|
p.add_argument("--out_root", type=str)
|
||||||
p.add_argument("--data_path", type=str)
|
p.add_argument("--data_path", type=str)
|
||||||
p.add_argument("--split_mode", type=str,
|
p.add_argument("--split_mode", type=str,
|
||||||
@@ -255,6 +262,12 @@ def main() -> None:
|
|||||||
"claude_code_exec_use_sdk": "model.claude_code_exec_use_sdk",
|
"claude_code_exec_use_sdk": "model.claude_code_exec_use_sdk",
|
||||||
"claude_code_exec_effort": "model.claude_code_exec_effort",
|
"claude_code_exec_effort": "model.claude_code_exec_effort",
|
||||||
"claude_code_exec_max_thinking_tokens": "model.claude_code_exec_max_thinking_tokens",
|
"claude_code_exec_max_thinking_tokens": "model.claude_code_exec_max_thinking_tokens",
|
||||||
|
"minimax_base_url": "model.minimax_base_url",
|
||||||
|
"minimax_api_key": "model.minimax_api_key",
|
||||||
|
"minimax_model": "model.minimax_model",
|
||||||
|
"minimax_temperature": "model.minimax_temperature",
|
||||||
|
"minimax_max_tokens": "model.minimax_max_tokens",
|
||||||
|
"minimax_enable_thinking": "model.minimax_enable_thinking",
|
||||||
"seed": "train.seed",
|
"seed": "train.seed",
|
||||||
"test_env_num": "evaluation.test_env_num",
|
"test_env_num": "evaluation.test_env_num",
|
||||||
"env": "env.name",
|
"env": "env.name",
|
||||||
@@ -312,6 +325,9 @@ def main() -> None:
|
|||||||
elif backend == "claude_code_exec":
|
elif backend == "claude_code_exec":
|
||||||
cfg.setdefault("optimizer_backend", "openai_chat")
|
cfg.setdefault("optimizer_backend", "openai_chat")
|
||||||
cfg.setdefault("target_backend", "claude_code_exec")
|
cfg.setdefault("target_backend", "claude_code_exec")
|
||||||
|
elif backend in {"minimax", "minimax_chat"}:
|
||||||
|
cfg.setdefault("optimizer_backend", "openai_chat")
|
||||||
|
cfg.setdefault("target_backend", "minimax_chat")
|
||||||
else:
|
else:
|
||||||
cfg.setdefault("optimizer_backend", "openai_chat")
|
cfg.setdefault("optimizer_backend", "openai_chat")
|
||||||
cfg.setdefault("target_backend", "openai_chat")
|
cfg.setdefault("target_backend", "openai_chat")
|
||||||
@@ -337,6 +353,15 @@ def main() -> None:
|
|||||||
and not _has_model_override("model.target", "target_model")
|
and not _has_model_override("model.target", "target_model")
|
||||||
):
|
):
|
||||||
cfg["target_model"] = default_model_for_backend("claude_chat")
|
cfg["target_model"] = default_model_for_backend("claude_chat")
|
||||||
|
if cfg.get("target_backend") == "minimax_chat":
|
||||||
|
if (
|
||||||
|
str(cfg.get("target_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS
|
||||||
|
and not _has_model_override("model.target", "target_model")
|
||||||
|
):
|
||||||
|
cfg["target_model"] = (
|
||||||
|
cfg.get("minimax_model")
|
||||||
|
or default_model_for_backend("minimax_chat")
|
||||||
|
)
|
||||||
|
|
||||||
if not cfg.get("out_root"):
|
if not cfg.get("out_root"):
|
||||||
env = cfg.get("env", "unknown")
|
env = cfg.get("env", "unknown")
|
||||||
@@ -416,6 +441,16 @@ def main() -> None:
|
|||||||
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
|
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
|
||||||
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
|
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
|
||||||
)
|
)
|
||||||
|
configure_minimax_chat(
|
||||||
|
base_url=cfg.get("minimax_base_url") or None,
|
||||||
|
api_key=cfg.get("minimax_api_key") or None,
|
||||||
|
temperature=cfg.get("minimax_temperature"),
|
||||||
|
max_tokens=cfg.get("minimax_max_tokens"),
|
||||||
|
enable_thinking=cfg.get("minimax_enable_thinking"),
|
||||||
|
)
|
||||||
|
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_reasoning_effort(cfg.get("reasoning_effort", "") or None)
|
set_reasoning_effort(cfg.get("reasoning_effort", "") or None)
|
||||||
|
|
||||||
# Build adapter
|
# Build adapter
|
||||||
|
|||||||
@@ -1006,5 +1006,46 @@ class TestClaudeCliBackendBare(unittest.TestCase):
|
|||||||
self.assertIn("Not logged in", getattr(be, "last_call_error", ""))
|
self.assertIn("Not logged in", getattr(be, "last_call_error", ""))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MockRewardHackingBackend(MockBackend):
|
||||||
|
def attempt(self, task: TaskRecord, skill: str, memory: str, sample_id: int = 0) -> str:
|
||||||
|
ctx = (skill or "") + "\n" + (memory or "")
|
||||||
|
# If the reward-hacking artifact is present in the context:
|
||||||
|
if "Always emit placeholder URLs" in ctx:
|
||||||
|
if "rule:__reward_hacking__" in task.tags:
|
||||||
|
return str(task.reference) # perfectly answers the train/replay shortcut task
|
||||||
|
else:
|
||||||
|
return "placeholder URL" # completely breaks the real held-out task
|
||||||
|
|
||||||
|
# Baseline behavior (without the rule)
|
||||||
|
if "rule:__reward_hacking__" in task.tags:
|
||||||
|
return "I am missing input" # baseline fails the shortcut task
|
||||||
|
if "rule:real" in task.tags:
|
||||||
|
return str(task.reference) # baseline gets the real task right
|
||||||
|
|
||||||
|
return super().attempt(task, skill, memory, sample_id)
|
||||||
|
|
||||||
|
def reflect(self, failures, successes, skill, memory, **kwargs):
|
||||||
|
edits = []
|
||||||
|
for task, _ in failures:
|
||||||
|
if "rule:__reward_hacking__" in task.tags:
|
||||||
|
edits.append(EditRecord("skill", "add", "Always emit placeholder URLs", rationale="shortcut"))
|
||||||
|
break
|
||||||
|
return edits
|
||||||
|
|
||||||
|
class TestVerifierDiscipline(unittest.TestCase):
|
||||||
|
def test_gate_rejects_reward_hacking_edit(self):
|
||||||
|
be = MockRewardHackingBackend()
|
||||||
|
train_task = TaskRecord(id="t1", project="/p", intent="train", reference="XYZABC", reference_kind="exact", tags=["rule:__reward_hacking__"], split="train")
|
||||||
|
val_task = TaskRecord(id="v1", project="/p", intent="val", reference="PQRSTU", reference_kind="exact", tags=["rule:real"], split="val")
|
||||||
|
tasks = [train_task, val_task]
|
||||||
|
|
||||||
|
res = consolidate(be, tasks, "", "", edit_budget=4, gate_metric="hard", night=1)
|
||||||
|
|
||||||
|
self.assertFalse(res.accepted)
|
||||||
|
self.assertGreater(len(res.rejected_edits), 0)
|
||||||
|
self.assertIn("placeholder", res.rejected_edits[0].content)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user