Skip to content
Closed
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
14 changes: 14 additions & 0 deletions agent/curator.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,20 @@ def _run_llm_review(prompt: str) -> Dict[str, Any]:
_m = _cfg.get("model", {}) if isinstance(_cfg.get("model"), dict) else {}
_provider = _m.get("provider") or "auto"
_model_name = _m.get("default") or _m.get("model") or ""
# Per-task auxiliary override: if curator.auxiliary.{provider,model}
# is set, prefer it over the main-model defaults so users can pin
# the curator to a cheaper aux model without changing their main.
# Either field can be set independently — provider falls back to
# the main provider, model falls back to the main model.
_cur = _cfg.get("curator") or {}
_aux = _cur.get("auxiliary") or {} if isinstance(_cur, dict) else {}
if isinstance(_aux, dict):
_aux_provider = _aux.get("provider")
_aux_model = _aux.get("model")
if _aux_provider:
_provider = _aux_provider
if _aux_model:
_model_name = _aux_model
_rp = resolve_runtime_provider(
requested=_provider, target_model=_model_name
)
Expand Down
106 changes: 106 additions & 0 deletions tests/agent/test_curator.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,112 @@ class _A:
assert "bundled" in captured.out.lower() or "hub" in captured.out.lower()


# ---------------------------------------------------------------------------
# _run_llm_review provider/model resolution — curator.auxiliary override (#17572)
# ---------------------------------------------------------------------------


def _patch_run_llm_review_deps(monkeypatch, cfg):
"""Patch the inner imports of _run_llm_review to capture AIAgent kwargs."""
captured = {}

class _StubAgent:
def __init__(self, **kwargs):
captured["agent_kwargs"] = kwargs
self._review_skipped = True

def disable_curator_review(self):
pass

def run_conversation(self, *a, **kw):
return {"final_response": "ok", "messages": []}

import hermes_cli.config as cfg_mod
import hermes_cli.runtime_provider as rp_mod
import run_agent

def _stub_resolve(requested=None, target_model=None, **_):
captured["resolve_args"] = {
"requested": requested,
"target_model": target_model,
}
return {
"api_key": "k",
"base_url": "https://example/api",
"api_mode": "chat_completions",
"provider": requested,
}

monkeypatch.setattr(cfg_mod, "load_config", lambda: cfg)
monkeypatch.setattr(rp_mod, "resolve_runtime_provider", _stub_resolve)
monkeypatch.setattr(run_agent, "AIAgent", _StubAgent)
return captured


def test_run_llm_review_uses_main_model_when_auxiliary_unset(
curator_env, monkeypatch
):
curator = curator_env["curator"]
cfg = {
"model": {"provider": "anthropic", "default": "claude-opus-4-7"},
"curator": {"auxiliary": {"provider": None, "model": None}},
}
captured = _patch_run_llm_review_deps(monkeypatch, cfg)
# Bypass the default neutralization installed by curator_env.
monkeypatch.undo()
monkeypatch.setattr(Path, "home", lambda: curator_env["home"].parent)
monkeypatch.setenv("HERMES_HOME", str(curator_env["home"]))
captured = _patch_run_llm_review_deps(monkeypatch, cfg)

curator._run_llm_review("prompt")
assert captured["resolve_args"]["requested"] == "anthropic"
assert captured["resolve_args"]["target_model"] == "claude-opus-4-7"
assert captured["agent_kwargs"]["model"] == "claude-opus-4-7"
assert captured["agent_kwargs"]["provider"] == "anthropic"


def test_run_llm_review_honors_auxiliary_model_override(
curator_env, monkeypatch
):
curator = curator_env["curator"]
cfg = {
"model": {"provider": "anthropic", "default": "claude-opus-4-7"},
"curator": {
"auxiliary": {"provider": "anthropic", "model": "claude-haiku-4-6"}
},
}
monkeypatch.undo()
monkeypatch.setattr(Path, "home", lambda: curator_env["home"].parent)
monkeypatch.setenv("HERMES_HOME", str(curator_env["home"]))
captured = _patch_run_llm_review_deps(monkeypatch, cfg)

curator._run_llm_review("prompt")
assert captured["resolve_args"]["target_model"] == "claude-haiku-4-6"
assert captured["agent_kwargs"]["model"] == "claude-haiku-4-6"
assert captured["agent_kwargs"]["provider"] == "anthropic"


def test_run_llm_review_partial_auxiliary_provider_only(
curator_env, monkeypatch
):
"""Provider override alone should keep the main model."""
curator = curator_env["curator"]
cfg = {
"model": {"provider": "anthropic", "default": "claude-opus-4-7"},
"curator": {
"auxiliary": {"provider": "openrouter", "model": None}
},
}
monkeypatch.undo()
monkeypatch.setattr(Path, "home", lambda: curator_env["home"].parent)
monkeypatch.setenv("HERMES_HOME", str(curator_env["home"]))
captured = _patch_run_llm_review_deps(monkeypatch, cfg)

curator._run_llm_review("prompt")
assert captured["resolve_args"]["requested"] == "openrouter"
assert captured["resolve_args"]["target_model"] == "claude-opus-4-7"


def test_cli_pin_refuses_bundled_skill(curator_env, capsys):
from hermes_cli import curator as cli
skills_dir = curator_env["home"] / "skills"
Expand Down