From 8e2eb4b511967a0ad776c0c667f6914072e1b7ec Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 11 May 2026 10:15:30 -0700 Subject: [PATCH] fix(/model): surface Nous Portal models from remote catalog manifest (#23912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /model picker for Nous Portal users was returning the in-repo _PROVIDER_MODELS["nous"] snapshot — which only updates on Hermes releases — instead of the remote manifest published at https://hermes-agent.nousresearch.com/docs/api/model-catalog.json. OpenRouter already pulled from the manifest via fetch_openrouter_models; "nous" was the only curated provider where the existing manifest plumbing (get_curated_nous_model_ids → get_curated_nous_models) was defined but not wired into the picker pipeline. Switch the curated build in list_authenticated_providers to use it, with the same graceful fallback to the in-repo snapshot when the manifest is unreachable. Test: tests/hermes_cli/test_model_catalog.py exercises the picker with a patched manifest and asserts the manifest's nous list reaches list_picker_providers. Falls-back-to-static path was already covered by test_curated_nous_ids_falls_back_to_hardcoded_on_empty_catalog. --- hermes_cli/model_switch.py | 10 ++++-- tests/hermes_cli/test_model_catalog.py | 46 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index d75aca5cd08..1bdb76b0f0f 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1079,6 +1079,7 @@ def list_authenticated_providers( from hermes_cli.models import ( OPENROUTER_MODELS, _PROVIDER_MODELS, _MODELS_DEV_PREFERRED, _merge_with_models_dev, provider_model_ids, + get_curated_nous_model_ids, ) results: List[dict] = [] @@ -1160,9 +1161,12 @@ def list_authenticated_providers( # Build curated model lists keyed by hermes provider ID curated: dict[str, list[str]] = dict(_PROVIDER_MODELS) curated["openrouter"] = [mid for mid, _ in OPENROUTER_MODELS] - # "nous" shares OpenRouter's curated list if not separately defined - if "nous" not in curated: - curated["nous"] = curated["openrouter"] + # "nous" pulls from the remote model-catalog manifest published at + # https://hermes-agent.nousresearch.com/docs/api/model-catalog.json so + # newly added Portal models surface in the /model picker without + # requiring a Hermes release. Falls back to the in-repo + # _PROVIDER_MODELS["nous"] snapshot when the manifest is unreachable. + curated["nous"] = get_curated_nous_model_ids() # Ollama Cloud uses dynamic discovery (no static curated list) if "ollama-cloud" not in curated: from hermes_cli.models import fetch_ollama_cloud_models diff --git a/tests/hermes_cli/test_model_catalog.py b/tests/hermes_cli/test_model_catalog.py index 2b757ac79b2..8910705c74d 100644 --- a/tests/hermes_cli/test_model_catalog.py +++ b/tests/hermes_cli/test_model_catalog.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os import time from pathlib import Path from unittest.mock import patch @@ -282,3 +283,48 @@ class TestIntegrationWithModelsModule: result = get_curated_nous_model_ids() assert result == ["anthropic/claude-opus-4.7", "moonshotai/kimi-k2.6"] + + def test_picker_nous_row_uses_manifest(self, tmp_path, monkeypatch): + """The /model picker must surface the manifest's nous list, not the + in-repo _PROVIDER_MODELS["nous"] snapshot. Regression: before this + fix, list_authenticated_providers() built the curated dict from + _PROVIDER_MODELS only — so newly-added Portal models never reached + the slash-command picker until the next Hermes release. + """ + # We deliberately do NOT use the ``isolated_home`` fixture here: + # that fixture monkeypatches ``Path.home`` to ``tmp_path``, which + # trips the auth-store seat-belt in ``_auth_file_path()`` because + # ``HERMES_HOME / auth.json`` then resolves to the same path the + # seat-belt thinks is the "real" user store. Use the autouse + # ``_hermetic_environment`` HERMES_HOME directly instead. + import importlib + from hermes_cli import model_catalog + importlib.reload(model_catalog) + try: + from hermes_cli.model_switch import list_picker_providers + + active_home = Path(os.environ["HERMES_HOME"]) + (active_home / "auth.json").write_text( + json.dumps( + { + "providers": {"nous": {"access_token": "fake"}}, + "credential_pool": {}, + } + ) + ) + + with patch.object( + model_catalog, "_fetch_manifest", return_value=_valid_manifest() + ): + picker = list_picker_providers( + current_provider="nous", max_models=99 + ) + finally: + model_catalog.reset_cache() + + nous_row = next((r for r in picker if r["slug"] == "nous"), None) + assert nous_row is not None, "nous row must appear when authed" + assert nous_row["models"] == [ + "anthropic/claude-opus-4.7", + "moonshotai/kimi-k2.6", + ]