# NousResearch/hermes-agent · curator auxiliary model config not being respected

## Open now

- 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.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #17868. Verify it addresses your case.
- **Recommended:** read PR #17868 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member 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.

## Closed / duplicate issues

- #17572: \[Bug\]: curator.auxiliary config is dead — curator always runs on main model

## Prior & competing PRs

- **#17868: fix(curator): unify under auxiliary.curator (hermes model, dashboard)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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`
  - fix hunks:
  - `agent/curator.py`

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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": {
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -952,6 +952,7 @@ def get_model_info():
         "approval",
         "mcp",
         "title_generation",
    +    "curator",
     )
     
     
    ```
  - `tests/agent/test_curator.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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
     
    ```
- **#17584: Fix curator LLM review to honor curator-specific auxiliary model config**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `0da968e52`, `#17868`, `#17572`
- **#17591: fix(curator): read curator.auxiliary config for reviewer model**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#17584`, `#17572`
- **#17781: fix(curator): honor curator.auxiliary.{provider,model} override (#17572)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `0da968e52`, `#17868`, `#17572`
- **#18039: fix(curator): respect curator.auxiliary config for review model**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#17868`

## Fix status

- Merged to main in PR #17868 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-30`.
