curator auxiliary model config not being respected

NousResearch/hermes-agent · cluster 17572

Data as of

Nothing open on GitHub. Fix merged in #17868. Check it covers your case.

The short answer

0open issues
0open PRs
6items grouped
Known?
Yes. 6 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #17868. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs focus on ensuring the curator reads and applies its own auxiliary config (provider, model, api_key, base_url) rather than falling back to the main model.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #17868 fix(curator): unify under auxiliary.curator (hermes model, dashboard)

    merged to main

    files: agent/curator.py, hermes_cli/config.py, hermes_cli/main.py, hermes_cli/web_server.py, tests/agent/test_curator.py, web/src/pages/ModelsPage.tsx, website/docs/user-guide/features/curator.md

    Show the diff
    • agent/curator.py
      @@ -714,6 +714,49 @@ def _llm_pass():
           }
       
       
      +def _resolve_review_model(cfg: Dict[str, Any]) -> tuple[str, str]:
      +    """Pick (provider, model) for the curator review fork.
      +
      +    Curator is a regular auxiliary task slot — ``auxiliary.curator.{provider,model}``
      +    — so it participates in the canonical aux-model plumbing (``hermes model`` →
      +    auxiliary picker, the dashboard Models tab, ``auxiliary.curator.{timeout,
      +    base_url,api_key,extra_body}``). ``provider: "auto"`` with an empty model
      +    means "use the main chat model" — same default as every other aux task.
      +
      +    Legacy fallback: users who configured ``curator.auxiliary.{provider,model}``
      +    under the previous one-off schema still work. Precedence:
      +      1. ``auxiliary.curator.{provider,model}`` when both are set non-auto
      +      2. Legacy ``curator.auxiliary.{provider,model}`` when both are set
      +      3. Main ``model.{provider,default/model}`` pair
      +    """
      +    _main = cfg.get("model", {}) if isinstance(cfg.get("model"), dict) else {}
      +    _main_provider = _main.get("provider") or "auto"
      +    _main_model = _main.get("default") or _main.get("model") or ""
      +
      +    # 1. Canonical aux task slot
      +    _aux = cfg.get("auxiliary", {}) if isinstance(cfg.get("auxiliary"), dict) else {}
      +    _cur_task = _aux.get("curator", {}) if isinstance(_aux.get("curator"), dict) else {}
      +    _task_provider = (_cur_task.get("provider") or "").strip() or None
      +    _task_model = (_cur_task.get("model") or "").strip() or None
      +    if _task_provider and _task_provider != "auto" and _task_model:
      +        return _task_provider, _task_model
      +
      +    # 2. Legacy curator.auxiliary.{provider,model} (deprecated, pre-unification)
      +    _cur = cfg.get("curator", {}) if isinstance(cfg.get("curator"), dict) else {}
      +    _legacy = _cur.get("auxiliary", {}) if isinstance(_cur.get("auxiliary"), dict) else {}
      +    _legacy_provider = _legacy.get("provider") or None
      +    _legacy_model = _legacy.get("model") or None
      +    if _legacy_provider and _legacy_model:
      +        logger.info(
      +            "curator: using deprecated curator.auxiliary.{provider,model} "
      +            "config — please migrate to auxiliary.curator.{provider,model}"
      +        )
      +        return _legacy_provider, _legacy_model
      +
      +    # 3. Fall through to the main chat model
      +    return _main_provider, _main_model
      +
      +
       def _run_llm_review(prompt: str) -> Dict[str, Any]:
           """Spawn an AIAgent fork to run the curator review prompt.
       
      @@ -749,6 +792,11 @@ def _run_llm_review(prompt: str) -> Dict[str, Any]:
           # "No models provided"). AIAgent() without explicit provider/model
           # arguments hits an auto-resolution path that fails for OAuth-only
           # providers and for pool-backed credentials.
      +    #
      +    # `_resolve_review_model()` honors `auxiliary.curator.{provider,model}`
      +    # (canonical aux-task slot, wired through `hermes model` → auxiliary
      +    # picker and the dashboard Models tab), with a legacy fallback to
      +    # `curator.auxiliary.{provider,model}`. See docs/user-guide/features/curator.md.
           _api_key = None
           _base_url = None
           _api_mode = None
      @@ -758,9 +806,7 @@ def _run_llm_review(prompt: str) -> Dict[str, Any]:
               from hermes_cli.config import load_config
               from hermes_cli.runtime_provider import resolve_runtime_provider
               _cfg = load_config()
      -        _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 ""
      +        _provider, _model_name = _resolve_review_model(_cfg)
               _rp = resolve_runtime_provider(
                   requested=_provider, target_model=_model_name
               )
    • hermes_cli/config.py
      @@ -709,6 +709,19 @@ def _ensure_hermes_home_managed(home: Path):
                   "timeout": 30,
                   "extra_body": {},
               },
      +        # Curator — skill-usage review fork. Timeout is generous because the
      +        # review pass can take several minutes on reasoning models (umbrella
      +        # building over hundreds of candidate skills). "auto" = use main chat
      +        # model; override via `hermes model` → auxiliary → Curator to route
      +        # to a cheaper aux model (e.g. openrouter google/gemini-3-flash-preview).
      +        "curator": {
      +            "provider": "auto",
      +            "model": "",
      +            "base_url": "",
      +            "api_key": "",
      +            "timeout": 600,
      +            "extra_body": {},
      +        },
           },
           
           "display": {
      @@ -949,12 +962,6 @@ def _ensure_hermes_home_managed(home: Path):
               # Archive a skill (move to skills/.archive/) after this many days
               # without use. Archived skills are recoverable — no auto-deletion.
               "archive_after_days": 90,
      -        # Optional per-task override for the curator's aux model. Leave null
      -        # to use Hermes' main auxiliary client resolution.
      -        "auxiliary": {
      -            "provider": None,
      -            "model": None,
      -        },
           },
       
           # Honcho AI-native memory -- reads ~/.honcho/config.json as single source of truth.
    • hermes_cli/main.py
      @@ -1927,6 +1927,7 @@ def _clear_stale_openai_base_url():
           ("mcp",              "MCP",              "MCP tool reasoning"),
           ("title_generation", "Title generation", "session titles"),
           ("skills_hub",       "Skills hub",       "skills search/install"),
      +    ("curator",          "Curator",          "skill-usage review pass"),
       ]
       
       
    • hermes_cli/web_server.py
      @@ -952,6 +952,7 @@ def get_model_info():
           "approval",
           "mcp",
           "title_generation",
      +    "curator",
       )
       
       
    • tests/agent/test_curator.py
      @@ -485,3 +485,153 @@ class _A:
           captured = capsys.readouterr()
           assert rc == 1
           assert "bundled" in captured.out.lower() or "hub" in captured.out.lower()
      +
      +
      +# ---------------------------------------------------------------------------
      +# curator review-model resolution (canonical auxiliary.curator slot)
      +#
      +# Curator was unified with the rest of the aux task system in Apr 2026 so
      +# `hermes model` → auxiliary picker, the dashboard Models tab, and the full
      +# per-task config (timeout, base_url, api_key, extra_body) all work for it.
      +# Voscko report: curator.auxiliary.{provider,model} was advertised but never
      +# read. Fix wires curator through auxiliary.curator with a legacy fallback.
      +# ---------------------------------------------------------------------------
      +
      +
      +def test_review_model_defaults_to_main_when_slot_is_auto(curator_env):
      +    """auxiliary.curator absent (or auto/empty) → use main model.provider/model."""
      +    curator = curator_env["curator"]
      +    cfg = {
      +        "model": {"provider": "openrouter", "default": "openai/gpt-5.5"},
      +    }
      +    assert curator._resolve_review_model(cfg) == ("openrouter", "openai/gpt-5.5")
      +
      +    # Explicit auto/empty slot — still main model.
      +    cfg["auxiliary"] = {"curator": {"provider": "auto", "model": ""}}
      +    assert curator._resolve_review_model(cfg) == ("openrouter", "openai/gpt-5.5")
      +
      +
      +def test_review_model_honors_auxiliary_curator_slot(curator_env):
      +    """auxiliary.curator.{provider,model} fully set → that pair wins."""
      +    curator = curator_env["curator"]
      +    cfg = {
      +        "model": {"provider": "openrouter", "default": "openai/gpt-5.5"},
      +        "auxiliary": {
      +            "curator": {
      +                "provider": "openrouter",
      +                "model": "openai/gpt-5.4-mini",
      +            },
      +        },
      +    }
      +    assert curator._resolve_review_model(cfg) == (
      +        "openrouter", "openai/gpt-5.4-mini",
      +    )
      +
      +
      +def test_review_model_auxiliary_curator_partial_override_falls_back(curator_env):
      +    """Only one of slot provider/model set → fall back to the main pair.
      +
      +    Prevents half-configured overrides from sending an empty side to
      +    resolve_runtime_provider.
      +    """
      +    curator = curator_env["curator"]
      +    base_main = {"provider": "openrouter", "default": "openai/gpt-5.5"}
      +
      +    cfg_provider_only = {
      +        "model": dict(base_main),
      +        "auxiliary": {"curator": {"provider": "openrouter", "model": ""}},
      +    }
      +    assert curator._resolve_review_model(cfg_provider_only) == (
      +        "openrouter", "openai/gpt-5.5",
      +    )
      +
      +    cfg_model_only = {
      +        "model": dict(base_main),
      +        "auxiliary": {"curator": {"provider": "auto", "model": "gpt-5.4-mini"}},
      +    }
      +    assert curator._resolve_review_model(cfg_model_only) == (
      +        "openrouter", "openai/gpt-5.5",
      +    )
      +
      +
      +def test_review_model_legacy_curator_auxiliary_still_works(curator_env, caplog):
      +    """Pre-unification users set curator.auxiliary.{provider,model} — honor it.
      +
      +    Emits a deprecation log line but keeps their config working.
      +    """
      +    curator = curator_env["curator"]
      +    cfg = {
      +        "model": {"provider": "openrouter", "default": "openai/gpt-5.5"},
      +        "curator": {
      +            "auxiliary": {
      +                "provider": "openrouter",
      +                "model": "openai/gpt-5.4-mini",
      +            },
      +        },
      +    }
      +    import logging
      +    with caplog.at_level(logging.INFO, logger="agent.curator"):
      +        result = curator._resolve_review_model(cfg)
      +    assert result == ("openrouter", "openai/gpt-5.4-mini")
      +    assert any(
      +        "deprecated curator.auxiliary" in rec.message for rec in caplog.records
      +    ), "expected deprecation warning when legacy curator.auxiliary is used"
      +
      +
      +def test_review_model_new_slot_wins_over_legacy(curator_env):
      +    """When BOTH new and legacy are set, the canonical slot wins."""
      +    curator = curator_env["curator"]
      +    cfg = {
      +        "model": {"provider": "openrouter", "default": "openai/gpt-5.5"},
      +        "auxiliary": {
      +            "curator": {"provider": "nous", "model": "new-winner"},
      +        },
      +        "curator": {
      +            "auxiliary": {"provider": "openrouter", "model": "legacy-loser"},
      +        },
      +    }
      +    assert curator._resolve_review_model(cfg) == ("nous", "new-winner")
      +
      +
      +def test_review_model_handles_missing_sections(curator_env):
      +    """Missing auxiliary/curator sections never raise — fall back cleanly."""
      +    curator = curator_env["curator"]
      +    cfg = {"model": {"provider": "anthropic", "model": "claude-sonnet-4-6"}}
      +    assert curator._resolve_review_model(cfg) == (
      +        "anthropic", "claude-sonnet-4-6",
      +    )
      +
      +    # Completely empty config → ("auto", "") — resolve_runtime_provider
      +    # handles the auto-detection chain from there.
      +    assert curator._resolve_review_model({}) == ("auto", "")
      +
      +
      +def test_curator_slot_is_canonical_aux_task():
      +    """Curator must be a first-class slot in every aux-task registry.
      +
      +    Four sources of truth, all checked by the shared registry test
      +    (test_aux_config.py) for the main tasks — this test pins `curator`
      +    specifically so the unification doesn't silently regress.
      +    """
      +    from hermes_cli.config import DEFAULT_CONFIG
      +    from hermes_cli.main import _AUX_TASKS
      +    from hermes_cli.web_server import _AUX_TASK_SLOTS
      +
      +    # 1. DEFAULT_CONFIG.auxiliary — schema source
      +    assert "curator" in DEFAULT_CONFIG["auxiliary"], \
      +        "curator missing from DEFAULT_CONFIG['auxiliary']"
      +    slot = DEFAULT_CONFIG["auxiliary"]["curator"]
      +    assert slot["provider"] == "auto"
      +    assert slot["model"] == ""
      +    assert slot["timeout"] > 0, "curator timeout should be set (reviews run long)"
      +
      +    # 2. hermes_cli/main.py _AUX_TASKS — CLI picker
      +    aux_keys = {k for k, _name, _desc in _AUX_TASKS}
      +    assert "curator" in aux_keys, "curator missing from _AUX_TASKS (CLI picker)"
      +
      +    # 3. hermes_cli/web_server.py _AUX_TASK_SLOTS — REST API allowlist
      +    assert "curator" in _AUX_TASK_SLOTS, \
      +        "curator missing from _AUX_TASK_SLOTS (dashboard REST API)"
      +
      +    # 4. web/src/pages/ModelsPage.tsx is checked at build time; the tsx
      +    #    array and this tuple share a ``Must match _AUX_TASK_SLOTS`` comment.
    • web/src/pages/ModelsPage.tsx
      @@ -44,6 +44,7 @@ const AUX_TASKS: readonly { key: string; label: string; hint: string }[] = [
         { key: "approval", label: "Approval", hint: "Smart auto-approve" },
         { key: "mcp", label: "MCP", hint: "MCP tool routing" },
         { key: "title_generation", label: "Title Gen", hint: "Session titles" },
      +  { key: "curator", label: "Curator", hint: "Skill-usage review" },
       ] as const;
       
       function formatTokens(n: number): string {
    • website/docs/user-guide/features/curator.md
      @@ -41,14 +41,38 @@ curator:
         min_idle_hours: 2
         stale_after_days: 30
         archive_after_days: 90
      -  auxiliary:
      -    provider: null             # null = use main auxiliary client resolution
      -    model: null
       ```
       
       To disable entirely, set `curator.enabled: false`.
       
      -To use a cheaper aux model for the LLM review pass instead of your main model, set `curator.auxiliary.provider` and `curator.auxiliary.model` to something specific (e.g. `openrouter` + `google/gemini-3-flash-preview`).
      +### Running the review on a cheaper aux model
      +
      +The curator's LLM review pass is a regular auxiliary task slot — `auxiliary.curator` — alongside Vision, Compression, Session Search, etc. "Auto" means "use my main chat model"; override the slot to pin a specific provider + model for the review pass instead.
      +
      +**Easiest — `hermes model`:**
      +
      +```bash
      +hermes model                   # → "Auxiliary models — side-task routing"
      +                               # → pick "Curator" → pick provider → pick model
      +```
      +
      +The same picker is available in the web dashboard under the **Models** tab.
      +
      +**Direct config.yaml (equivalent):**
      +
      +```yaml
      +auxiliary:
      +  curator:
      +    provider: openrouter
      +    model: google/gemini-3-flash-preview
      +    timeout: 600               # generous — reviews can take several minutes
      +```
      +
      +Leaving `provider: auto` (the default) routes the review pass through whatever your main chat model is, matching the behavior of every other auxiliary task.
      +
      +:::note Legacy config
      +Earlier releases used a one-off `curator.auxiliary.{provider,model}` block. That path still works but emits a deprecation log line — please migrate to `auxiliary.curator` above so the curator shares the same plumbing (`hermes model`, dashboard Models tab, `base_url`, `api_key`, `timeout`, `extra_body`) as every other aux task.
      +:::
       
       ## CLI
       

4 PRs closed without merging

From the close comments: superseded by another PR 2 · done elsewhere 1 · duplicate 1

  • #17584 Fix curator LLM review to honor curator-specific auxiliary model config · superseded by another PR → 0da968e, #17868, #17572
  • #17591 fix(curator): read curator.auxiliary config for reviewer model · duplicate → #17584, #17572
  • #17781 fix(curator): honor curator.auxiliary.{provider,model} override (#17572) · superseded by another PR → 0da968e, #17868, #17572
  • #18039 fix(curator): respect curator.auxiliary config for review model · done elsewhere → #17868
Closed issues · 1
  • Closed#17572[Bug]: curator.auxiliary config is dead — curator always runs on main model
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#17868
Closed issues
1
Attempts
4 PRs closed without merging
Fix link checked
30 Apr 2026

For your agent

The same facts, typed.