# NousResearch/hermes-agent · Silent default model selection and cost-safety concerns

## Open now

- Nothing open on GitHub.

These items concern which model is chosen when no explicit model is specified, with recurring tension between cost-safety (avoiding billing users unexpectedly) and keeping the silent default current as provider offerings change.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #64771. Verify it addresses your case.
- **Recommended:** read PR #64771 (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.67`: 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

- #58364: Silent default for nous provider pins deepseek/deepseek-v4-flash, which is no longer free — the cost-safety guard now bills users

## Prior & competing PRs

- **#64771: feat(models): catalog-labeled silent default (GLM-5.2) + bare-provider /model cost-safe routing (#61141 salvage)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/model_catalog.py`, `hermes_cli/models.py`, `hermes_cli/web_server.py`, `scripts/build_model_catalog.py`, `tests/hermes_cli/test_model_catalog.py`, `tests/hermes_cli/test_models.py`, `tests/test_empty_model_fallback.py`, `tui_gateway/server.py`, `website/docs/reference/model-catalog.md`, `website/static/api/model-catalog.json`
  - fix hunks:
  - `hermes_cli/model_catalog.py`

    ```diff
    @@ -356,6 +356,42 @@ def get_curated_nous_models() -> list[str] | None:
         return out or None
     
     
    +def _default_model_from_block(block: dict[str, Any] | None) -> str | None:
    +    """Return the id of the model entry labeled ``"default": true``, or None."""
    +    if not isinstance(block, dict):
    +        return None
    +    for m in block.get("models", []):
    +        if isinstance(m, dict) and m.get("default"):
    +            mid = str(m.get("id") or "").strip()
    +            if mid:
    +                return mid
    +    return None
    +
    +
    +def get_default_model_from_cache(provider: str) -> str | None:
    +    """Return the catalog's labeled default model for ``provider`` — cache only.
    +
    +    The manifest marks exactly one model entry per provider with
    +    ``"default": true``; that entry is the model Hermes silently lands on when
    +    the user never picked one. This accessor reads ONLY the in-process copy or
    +    the disk cache — it NEVER triggers a network fetch, so it is safe on hot
    +    resolution paths (agent build, gateway session setup) that must stay
    +    network-free. The cache is kept fresh by the picker/`hermes update` paths;
    +    when no cached manifest exists (fresh install, offline), returns None and
    +    the caller falls back to the in-repo constant.
    +    """
    +    if _catalog_cache is not None:
    +        block = _catalog_cache.get("providers", {}).get(provider)
    +        found = _default_model_from_block(block)
    +        if found:
    +            return found
    +    disk_data, _mtime = _read_disk_cache()
    +    if disk_data is not None:
    +        block = disk_data.get("providers", {}).get(provider)
    +        return _default_model_from_block(block)
    +    return None
    +
    +
     def seed_cache_from_checkout(project_root: "Path | str") -> bool:
         """Overwrite the disk cache with the catalog shipped in a local checkout.
     
    ```
  - `hermes_cli/models.py`

    ```diff
    @@ -74,7 +74,7 @@ def _urlopen_model_catalog_request(req: urllib.request.Request, *, timeout: floa
         # MiniMax
         ("minimax/minimax-m3",                     ""),
         # Z-AI
    -    ("z-ai/glm-5.2",                           ""),
    +    ("z-ai/glm-5.2",                           "default"),
         ("z-ai/glm-5.1",                           ""),
         # Xiaomi
         ("xiaomi/mimo-v2.5-pro",                   ""),
    ```

    ```diff
    @@ -1301,9 +1301,14 @@ def group_providers(slugs):
     }
     
     
    -# The model Hermes silently lands on when the user never picked one and the
    -# provider's catalog carries it (GUI onboarding confirm card, empty
    -# ``model.default``, provider-set-but-model-missing resolution). Deliberately a
    +# In-repo fallback for the model Hermes silently lands on when the user never
    +# picked one (GUI onboarding confirm card, empty ``model.default``,
    +# provider-set-but-model-missing resolution). The AUTHORITATIVE source is the
    +# remote model catalog: the manifest labels exactly one entry per provider
    +# with ``"default": true`` (see get_default_model_from_cache in
    +# model_catalog.py), so maintainers can rotate the default without shipping a
    +# release. This constant is the offline/fresh-install fallback and MUST match
    +# the labeled entry in website/static/api/model-catalog.json. Deliberately a
     # capable low-cost model rather than the curated lists' entry [0]: aggregator
     # lists are ordered most-capable-first, so [0] is the priciest Anthropic
     # flagship (claude-fable-5 / opus) — silently billing the most expensive model
    ```

    ```diff
    @@ -1311,43 +1316,57 @@ def group_providers(slugs):
     PREFERRED_SILENT_DEFAULT_MODEL = "z-ai/glm-5.2"
     
     
    -def pick_silent_default_model(model_ids: list[str]) -> str:
    +def get_preferred_silent_default_model(provider: str = "openrouter") -> str:
    +    """Return the silent-default model id — catalog label first, constant second.
    +
    +    Reads the ``"default": true`` label from the cached remote catalog
    +    (never hits the network — safe on hot resolution paths), falling back to
    +    :data:`PREFERRED_SILENT_DEFAULT_MODEL` when no cached manifest exists or
    +    the provider block carries no label.
    +    """
    +    try:
    +        from hermes_cli.model_catalog import get_default_model_from_cache
    +        labeled = get_default_model_from_cache(provider)
    +        if labeled:
    +            return labeled
    +    except Exception:
    +        pass
    +    return PREFERRED_SILENT_DEFAULT_MODEL
    +
    +
    +def pick_silent_default_model(model_ids: list[str], provider: str = "openrouter") -> str:
         """Pick the silent default from an available-models list.
     
    -    Returns :data:`PREFERRED_SILENT_DEFAULT_MODEL` when the list carries it,
    +    Returns the catalog-labeled default (see
    +    :func:`get_preferred_silent_default_model`) when the list carries it,
         else the first entry, else "". Used by every surface that must choose a
         model on the user's behalf without an interactive picker (GUI onboarding
         recommended-default, empty-model runtime fallback).
         """
    -    if PREFERRED_SILENT_DEFAULT_MODEL in model_ids:
    -        return PREFERRED_SILENT_DEFAULT_MODEL
    +    preferred = get_preferred_silent_default_model(provider)
    +    if preferred in model_ids:
    +        return preferred
         return model_ids[0] if model_ids else ""
     
     
    -# Cost-safe overrides for the *silent* auto-default
    -# (``get_default_model_for_provider``). Most providers' curated lists lead with a
    -# sensible default, but metered aggregators (Nous Portal, OpenRouter) order
    +# Providers whose *silent* auto-default must go through the cost-safe
    +# catalog-labeled default (``get_preferred_silent_default_model``) instead of
    +# curated-list entry [0]. Metered aggregators (Nous Portal, OpenRouter) order
     # their lists best-/most-capable-first — entry [0] is the priciest flagship
     # (``anthropic/claude-fable-5``). Using that as the non-interactive fallback
     # when a profile sets a provider with no model silently bills the most
     # expensive model for traffic the user never opted into (a missing default
    -# escalated to Opus and billed 863 requests before the user noticed). Pin the
    -# silent default to ``PREFERRED_SILENT_DEFAULT_MODEL`` instead so a missing
    -# model can never escalate to the flagship.
    +# escalated to Opus and billed 863 requests before the user noticed). The
    +# catalog manifest labels the default entry (``"default": true``) so it can
    +# rotate without a release; a missing model must never escalate to the
    +# flagship.
     #
    -# This is deliberately a fixed, side-effect-free default for the hot resolution
    -# path. The *interactive* default (GUI onboarding / ``hermes model``) uses the
    -# richer free/paid-tier-aware resolver — see ``get_recommended_default_model``
    -# in hermes_cli/web_server.py and ``partition_nous_models_by_tier`` — which can
    -# hit the Portal; this fallback must stay cheap and network-free.
    -_PROVIDER_SILENT_DEFAULT_OVERRIDES: dict[str, str] = {
    -    "nous": PREFERRED_SILENT_DEFAULT_MODEL,
    -    # OpenRouter has no static ``_PROVIDER_MODELS`` entry (its picker list is
    -    # fetched live), but the curated snapshot (``OPENROUTER_MODELS``) carries
    -    # the preferred default — trust the override so provider-set-but-model-
    -    # missing setups land on it instead of resolving to "".
    -    "openrouter": PREFERRED_SILENT_DEFAULT_MODEL,
    -}
    +# This is deliberately a network-free lookup for the hot resolution path
    +# (cache-only catalog read). The *interactive* default (GUI onboarding /
    +# ``hermes model``) uses the richer free/paid-tier-aware resolver — see
    +# ``get_recommended_default_model`` in hermes_cli/web_server.py and
    +# ``partition_nous_models_by_tier`` — which can hit the Portal.
    +_SILENT_DEFAULT_PROVIDERS: frozenset[str] = frozenset({"nous", "openrouter"})
     
     
     def get_default_model_for_provider(provider: str) -> str:
    ```

    ```diff
    @@ -1360,17 +1379,19 @@ def get_default_model_for_provider(provider: str) -> str:
         For most providers this is the first entry in ``_PROVIDER_MODELS`` — the
         same model the ``hermes model`` picker offers first. For metered aggregators
         whose curated list is ordered most-capable-first, that entry is also the
    -    most EXPENSIVE one, so silently defaulting to it is a billing footgun. Such
    -    providers carry an explicit override in
    -    ``_PROVIDER_SILENT_DEFAULT_OVERRIDES``; a missing model must never
    -    auto-escalate to the flagship.
    +    most EXPENSIVE one, so silently defaulting to it is a billing footgun.
    +    Those providers (``_SILENT_DEFAULT_PROVIDERS``) resolve through the
    +    catalog-labeled default instead; a missing model must never auto-escalate
    +    to the flagship.
         """
         models = _PROVIDER_MODELS.get(provider, [])
    -    override = _PROVIDER_SILENT_DEFAULT_OVERRIDES.get(provider)
    -    # Trust the override when the provider has no static catalog (OpenRouter's
    -    # picker list is fetched live; its curated snapshot carries the override).
    -    if override and (override in models or not models):
    -        return override
    +    if provider in _SILENT_DEFAULT_PROVIDERS:
    +        preferred = get_preferred_silent_default_model(provider)
    +        # Trust the preferred default even when the provider has no static
    +        # catalog (OpenRouter's picker list is fetched live; its curated
    +        # snapshot carries the default).
    +        if preferred and (preferred in models or not models):
    +            return preferred
         return models[0] if models else ""
     
     
    ```

    ```diff
    @@ -1456,6 +1477,7 @@ def fetch_openrouter_models(
             live_by_id[mid] = item
     
         curated: list[tuple[str, str]] = []
    +    silent_default = get_preferred_silent_default_model("openrouter")
         for preferred_id in preferred_ids:
             live_item = live_by_id.get(preferred_id)
             if live_item is None:
    ```

    ```diff
    @@ -1465,14 +1487,20 @@ def fetch_openrouter_models(
             # when the user selects them. Ported from Kilo-Org/kilocode#9068.
             if not _openrouter_model_supports_tools(live_item):
                 continue
    -        desc = "free" if _openrouter_model_is_free(live_item.get("pricing")) else ""
    +        if preferred_id == silent_default:
    +            # Keep the silent-default badge through the live refresh so the
    +            # picker shows which model Hermes lands on when none is selected.
    +            desc = "default"
    +        else:
    +            desc = "free" if _openrouter_model_is_free(live_item.get("pricing")) else ""
             curated.append((preferred_id, desc))
     
         if not curated:
             return list(_openrouter_catalog_cache or fallback)
     
    -    first_id, _ = curated[0]
    -    curated[0] = (first_id, "recommended")
    +    first_id, first_desc = curated[0]
    +    if not first_desc:
    +        curated[0] = (first_id, "recommended")
         _openrouter_catalog_cache = curated
         return list(curated)
     
    ```

    ```diff
    @@ -1976,7 +2004,18 @@ def detect_static_provider_for_model(
                 and default_models
                 and resolved_provider not in current_keys
             ):
    -            return (resolved_provider, default_models[0])
    +            # Route through the cost-safe default rather than picking
    +            # ``default_models[0]`` directly. For metered aggregators whose
    +            # curated list is ordered most-capable-first (e.g. Nous Portal),
    +            # entry [0] is the priciest flagship, and typing ``/model nous``
    +            # would silently escalate to it — the exact billing footgun the
    +            # catalog-labeled silent default (``_SILENT_DEFAULT_PROVIDERS``)
    +            # exists to prevent. For providers outside that set this is
    +            # unchanged (it returns ``models[0]``).
    +            return (
    +                resolved_provider,
    +                get_default_model_for_provider(resolved_provider) or default_models[0],
    +            )
     
         # Aggregators list other providers' models — never auto-switch TO them
         # If the model belongs to the current provider's catalog, don't suggest switching
    ```
  - `hermes_cli/web_server.py`

    ```diff
    @@ -5605,7 +5605,7 @@ def get_recommended_default_model(provider: str = ""):
                         model_ids, pricing, portal_url
                     )
     
    -            model = pick_silent_default_model(model_ids)
    +            model = pick_silent_default_model(model_ids, provider="nous")
                 return {"provider": "nous", "model": model, "free_tier": bool(free_tier)}
             except Exception:
                 _log.exception("GET /api/model/recommended-default (nous) failed")
    ```

    ```diff
    @@ -5623,7 +5623,7 @@ def get_recommended_default_model(provider: str = ""):
             for row in payload.get("providers", []):
                 if str(row.get("slug", "")).lower() == slug:
                     models = [str(m) for m in (row.get("models") or [])]
    -                return {"provider": slug, "model": pick_silent_default_model(models), "free_tier": None}
    +                return {"provider": slug, "model": pick_silent_default_model(models, provider=slug), "free_tier": None}
             return {"provider": slug, "model": "", "free_tier": None}
         except Exception:
             _log.exception("GET /api/model/recommended-default failed")
    ```
  - `scripts/build_model_catalog.py`

    ```diff
    @@ -33,12 +33,31 @@
     # Ensure HERMES_HOME is set for imports that touch it at module level.
     os.environ.setdefault("HERMES_HOME", os.path.join(os.path.expanduser("~"), ".hermes"))
     
    -from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS  # noqa: E402
    +from hermes_cli.models import (  # noqa: E402
    +    OPENROUTER_MODELS,
    +    PREFERRED_SILENT_DEFAULT_MODEL,
    +    _PROVIDER_MODELS,
    +)
     
     OUTPUT_PATH = os.path.join(REPO_ROOT, "website", "static", "api", "model-catalog.json")
     CATALOG_VERSION = 1
     
     
    +def _openrouter_entry(mid: str, desc: str) -> dict:
    +    entry: dict = {"id": mid, "description": desc}
    +    if mid == PREFERRED_SILENT_DEFAULT_MODEL:
    +        entry["description"] = desc or "default"
    +        entry["default"] = True
    +    return entry
    +
    +
    +def _nous_entry(mid: str) -> dict:
    +    entry: dict = {"id": mid}
    +    if mid == PREFERRED_SILENT_DEFAULT_MODEL:
    +        entry["default"] = True
    +    return entry
    +
    +
     def build_catalog() -> dict:
         return {
             "version": CATALOG_VERSION,
    ```

    ```diff
    @@ -53,11 +72,13 @@ def build_catalog() -> dict:
                         "display_name": "OpenRouter",
                         "note": (
                             "Descriptions drive picker badges. Live /api/v1/models "
    -                        "filters curated ids by tool-calling support and free pricing."
    +                        "filters curated ids by tool-calling support and free pricing. "
    +                        'The entry labeled "default": true is the model Hermes '
    +                        "silently lands on when the user never picked one."
                         ),
                     },
                     "models": [
    -                    {"id": mid, "description": desc}
    +                    _openrouter_entry(mid, desc)
                         for mid, desc in OPENROUTER_MODELS
                     ],
                 },
    ```

    ```diff
    @@ -66,11 +87,13 @@ def build_catalog() -> dict:
                         "display_name": "Nous Portal",
                         "note": (
                             "Free-tier gating is determined live via Portal pricing "
    -                        "(partition_nous_models_by_tier), not this manifest."
    +                        "(partition_nous_models_by_tier), not this manifest. "
    +                        'The entry labeled "default": true is the model Hermes '
    +                        "silently lands on when the user never picked one."
                         ),
                     },
                     "models": [
    -                    {"id": mid}
    +                    _nous_entry(mid)
                         for mid in _PROVIDER_MODELS.get("nous", [])
                     ],
                 },
    ```
  - `tests/hermes_cli/test_model_catalog.py`

    ```diff
    @@ -288,6 +288,68 @@ def test_nous_returns_none_when_catalog_empty(self, isolated_home):
                 assert model_catalog.get_curated_nous_models() is None
     
     
    +class TestDefaultModelFromCache:
    +    """get_default_model_from_cache reads the '"default": true' label without
    +    ever hitting the network."""
    +
    +    def _manifest_with_default(self) -> dict:
    +        m = _valid_manifest()
    +        m["providers"]["openrouter"]["models"][1]["default"] = True  # gpt-5.4
    +        m["providers"]["nous"]["models"][1]["default"] = True  # kimi-k2.6
    +        return m
    +
    +    def test_reads_label_from_disk_cache(self, isolated_home):
    +        from hermes_cli import model_catalog
    +        cache = isolated_home / "cache"
    +        cache.mkdir()
    +        (cache / "model_catalog.json").write_text(
    +            json.dumps(self._manifest_with_default())
    +        )
    +        with patch.object(model_catalog, "_fetch_manifest") as fetch:
    +            assert (
    +                model_catalog.get_default_model_from_cache("openrouter")
    +                == "openai/gpt-5.4"
    +            )
    +            assert (
    +                model_catalog.get_default_model_from_cache("nous")
    +                == "moonshotai/kimi-k2.6"
    +            )
    +            fetch.assert_not_called()
    +
    +    def test_no_label_returns_none(self, isolated_home):
    +        from hermes_cli import model_catalog
    +        cache = isolated_home / "cache"
    +        cache.mkdir()
    +        (cache / "model_catalog.json").write_text(json.dumps(_valid_manifest()))
    +        with patch.object(model_catalog, "_fetch_manifest") as fetch:
    +            assert model_catalog.get_default_model_from_cache("openrouter") is None
    +            fetch.assert_not_called()
    +
    +    def test_no_cache_returns_none_without_network(self, isolated_home):
    +        from hermes_cli import model_catalog
    +        with patch.object(model_catalog, "_fetch_manifest") as fetch:
    +            assert model_catalog.get_default_model_from_cache("openrouter") is None
    +            fetch.assert_not_called()
    +
    +    def test_shipped_manifest_labels_glm52_default(self, isolated_home):
    +        """Contract with the in-repo manifest: both provider blocks label the
    +        same default entry the code constant points at."""
    +        import hermes_cli.model_catalog as model_catalog
    +        from hermes_cli.models import PREFERRED_SILENT_DEFAULT_MODEL
    +
    +        repo_root = Path(model_catalog.__file__).resolve().parent.parent
    +        manifest = json.loads(
    +            (repo_root / "website" / "static" / "api" / "model-catalog.json").read_text()
    +        )
    +        for provider in ("openrouter", "nous"):
    +            block = manifest["providers"][provider]
    +            labeled = [m["id"] for m in block["models"] if m.get("default")]
    +            assert labeled == [PREFERRED_SILENT_DEFAULT_MODEL], (
    +                f"{provider}: exactly one entry must be labeled default and it "
    +                f"must match PREFERRED_SILENT_DEFAULT_MODEL"
    +            )
    +
    +
     class TestDisabled:
         def test_disabled_config_short_circuits(self, isolated_home):
             from hermes_cli import model_catalog
    ```
  - `tests/hermes_cli/test_models.py`

    ```diff
    @@ -82,7 +82,10 @@ def read(self):
     
         def test_falls_back_to_static_snapshot_on_fetch_failure(self, monkeypatch):
             monkeypatch.setattr(_models_mod, "_openrouter_catalog_cache", None)
    -        with patch("hermes_cli.models._urlopen_model_catalog_request", side_effect=OSError("boom")):
    +        # Pin the remote manifest out too — otherwise the fallback silently
    +        # depends on whatever the deployed catalog currently contains.
    +        with patch("hermes_cli.model_catalog.get_curated_openrouter_models", return_value=None), \
    +             patch("hermes_cli.models._urlopen_model_catalog_request", side_effect=OSError("boom")):
                 models = fetch_openrouter_models(force_refresh=True)
     
             assert models == OPENROUTER_MODELS
    ```
  - `tests/test_empty_model_fallback.py`

    ```diff
    @@ -38,14 +38,14 @@ def test_custom_provider_returns_empty(self):
         def test_nous_silent_default_is_not_the_expensive_flagship(self):
             """Nous Portal is a metered aggregator whose curated list is ordered
             most-capable-first, so entry [0] is the priciest flagship
    -        (anthropic/claude-opus-4.8). The silent fallback (provider set, no model)
    +        (anthropic/claude-fable-5). The silent fallback (provider set, no model)
             must NOT escalate to it — otherwise an unconfigured profile silently
             bills the most expensive model. Regression for the billing footgun.
             """
             from hermes_cli.models import (
                 _PROVIDER_MODELS,
    -            _PROVIDER_SILENT_DEFAULT_OVERRIDES,
                 get_default_model_for_provider,
    +            get_preferred_silent_default_model,
             )
     
             result = get_default_model_for_provider("nous")
    ```

    ```diff
    @@ -53,27 +53,108 @@ def test_nous_silent_default_is_not_the_expensive_flagship(self):
             assert "opus" not in result.lower(), (
                 f"silent default escalated to an expensive flagship: {result!r}"
             )
    +        assert "claude" not in result.lower(), (
    +            f"silent default escalated to an expensive flagship: {result!r}"
    +        )
             assert result != _PROVIDER_MODELS["nous"][0], (
                 "silent default must not be the most-capable/priciest catalog entry"
             )
    -        # The override must point at a model that actually exists in the catalog.
    -        assert result == _PROVIDER_SILENT_DEFAULT_OVERRIDES["nous"]
    +        # The default must resolve through the catalog-label helper and point
    +        # at a model that actually exists in the curated catalog.
    +        assert result == get_preferred_silent_default_model("nous")
             assert result in _PROVIDER_MODELS["nous"]
     
    -    def test_override_falls_back_to_catalog_when_missing(self):
    -        """If an override model is no longer in the catalog, fall back to [0]
    -        rather than returning a stale/absent id."""
    +    def test_catalog_label_overrides_constant(self):
    +        """A ``"default": true`` label in the cached catalog manifest wins over
    +        the in-repo constant, so maintainers can rotate the silent default
    +        without shipping a release."""
    +        from unittest.mock import patch
    +
    +        from hermes_cli import models as models_mod
    +
    +        with patch(
    +            "hermes_cli.model_catalog.get_default_model_from_cache",
    +            return_value="qwen/qwen3.7-plus",
    +        ):
    +            assert (
    +                models_mod.get_preferred_silent_default_model("nous")
    +                == "qwen/qwen3.7-plus"
    +            )
    +            # nous catalog carries qwen3.7-plus, so the full resolver follows.
    +            assert (
    +                models_mod.get_default_model_for_provider("nous")
    +                == "qwen/qwen3.7-plus"
    +            )
    +
    +    def test_no_catalog_cache_falls_back_to_constant(self):
    +        """With no cached manifest (fresh install / offline), the in-repo
    +        constant is the silent default."""
    +        from unittest.mock import patch
    +
    +        from hermes_cli import models as models_mod
    +
    +        with patch(
    +            "hermes_cli.model_catalog.get_default_model_from_cache",
    +            return_value=None,
    +        ):
    +            assert (
    +                models_mod.get_preferred_silent_default_model("openrouter")
    +                == models_mod.PREFERRED_SILENT_DEFAULT_MODEL
    +            )
    +
    +    def test_stale_label_not_in_catalog_falls_back(self):
    +        """If the labeled default model is no longer in the provider's curated
    +        catalog, fall back to entry [0] rather than returning an absent id."""
             from unittest.mock import patch
     
             from hermes_cli import models as models_mod
     
    -        with patch.dict(
    -            models_mod._PROVIDER_SILENT_DEFAULT_OVERRIDES,
    -            {"openai-codex": "does-not-exist-model"},
    -            clear=False,
    +        with patch(
    +            "hermes_cli.model_catalog.get_default_model_from_cache",
    +            return_value="does-not-exist-model",
             ):
    -            result = models_mod.get_default_model_for_provider("openai-codex")
    -            assert result == models_mod._PROVIDER_MODELS["openai-codex"][0]
    +            result = models_mod.get_default_model_for_provider("nous")
    +            assert result == models_mod._PROVIDER_MODELS["nous"][0]
    +
    +
    +class TestDetectStaticProviderCostSafeDefault:
    +    """detect_static_provider_for_model must apply the same cost-safe default
    +    as get_default_model_for_provider when a bare provider name is typed as a
    +    model (e.g. ``/model nous``)."""
    +
    +    def test_bare_nous_does_not_escalate_to_flagship(self):
    +        from hermes_cli.models import (
    +            _PROVIDER_MODELS,
    +            get_default_model_for_provider,
    +            detect_static_provider_for_model,
    +        )
    +
    +        result = detect_static_provider_for_model("nous", "openrouter")
    +        assert result is not None
    +        provider, model = result
    +        assert provider == "nous"
    +        # Must match the cost-safe silent default, NOT the priciest catalog
    +        # entry [0]. Regression: this path returned _PROVIDER_MODELS["nous"][0]
    +        # directly, re-introducing the billing footgun on the interactive
    +        # ``/model nous`` path.
    +        assert model == get_default_model_for_provider("nous")
    +        assert "opus" not in model.lower()
    +        assert model != _PROVIDER_MODELS["nous"][0]
    +
    +    def test_provider_without_override_still_uses_first_model(self):
    +        """Providers outside _SILENT_DEFAULT_PROVIDERS are unchanged."""
    +        from hermes_cli.models import (
    +            _PROVIDER_MODELS,
    +            _SILENT_DEFAULT_PROVIDERS,
    +            detect_static_provider_for_model,
    +        )
    +
    +        for provider in ("anthropic", "xai"):
    +            if provider in _SILENT_DEFAULT_PROVIDERS:
    +                continue
    +            result = detect_static_provider_for_model(provider, "openrouter")
    +            assert result is not None
    +            assert result[1] == _PROVIDER_MODELS[provider][0]
     
     
     class TestGatewayEmptyModelFallback:
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -2118,11 +2118,12 @@ def _resolve_model() -> str:
         if isinstance(m, str) and m:
             return m.strip()
         # No env seed and no config preference: fall back to the cost-safe silent
    -    # default, never an expensive Anthropic flagship the user didn't pick.
    +    # default (catalog-labeled, cache-only read), never an expensive Anthropic
    +    # flagship the user didn't pick.
         try:
    -        from hermes_cli.models import PREFERRED_SILENT_DEFAULT_MODEL
    +        from hermes_cli.models import get_preferred_silent_default_model
     
    -        return PREFERRED_SILENT_DEFAULT_MODEL
    +        return get_preferred_silent_default_model()
         except Exception:
             return "z-ai/glm-5.2"
     
    ```
  - `website/docs/reference/model-catalog.md`

    ```diff
    @@ -29,6 +29,7 @@ Published on every merge to `main` via the existing `deploy-site.yml` GitHub Pag
         "openrouter": {
           "metadata": {},
           "models": [
    +        {"id": "z-ai/glm-5.2",         "description": "default", "default": true},
             {"id": "moonshotai/kimi-k2.6", "description": "recommended", "metadata": {}},
             {"id": "openai/gpt-5.4",       "description": ""}
           ]
    ```

    ```diff
    @@ -36,6 +37,7 @@ Published on every merge to `main` via the existing `deploy-site.yml` GitHub Pag
         "nous": {
           "metadata": {},
           "models": [
    +        {"id": "z-ai/glm-5.2", "default": true},
             {"id": "anthropic/claude-opus-4.7"},
             {"id": "moonshotai/kimi-k2.6"}
           ]
    ```

    ```diff
    @@ -48,7 +50,8 @@ Field notes:
     
     - **`version`** — integer schema version. Future schemas bump this; Hermes refuses manifests with versions it doesn't understand and falls back to the hardcoded snapshot.
     - **`metadata`** — free-form dict at the manifest, provider, and model level. Any keys. Hermes ignores unknown fields, so you can annotate entries (`"tier": "paid"`, `"tags": [...]`, etc.) without coordinating a schema change.
    -- **`description`** — OpenRouter-only. Drives picker badge text (`"recommended"`, `"free"`, or empty). Nous Portal doesn't use this — free-tier gating is determined live from the Portal's pricing endpoint.
    +- **`description`** — OpenRouter-only. Drives picker badge text (`"recommended"`, `"free"`, `"default"`, or empty). Nous Portal doesn't use this — free-tier gating is determined live from the Portal's pricing endpoint.
    +- **`default`** — exactly one entry per provider may carry `"default": true`. That model is the **silent default**: what Hermes lands on when the user never selected a model (GUI onboarding confirm card, `provider` configured with no `model`, empty `model.default`). Read cache-only at runtime (`get_default_model_from_cache`) so hot resolution paths never hit the network; when no cached manifest exists, Hermes falls back to the in-repo `PREFERRED_SILENT_DEFAULT_MODEL` constant, which must match the labeled entry. This lets maintainers rotate the silent default without shipping a release. It is deliberately a capable low-cost model, never the priciest flagship.
     - **Pricing and context length** are NOT in the manifest. Those come from live provider APIs (`/v1/models` endpoints, models.dev) at fetch time.
     
     ## Fetch behavior
    ```
  - `website/static/api/model-catalog.json`

    ```diff
    @@ -1,6 +1,6 @@
     {
       "version": 1,
    -  "updated_at": "2026-07-09T18:38:59Z",
    +  "updated_at": "2026-07-15T04:51:12Z",
       "metadata": {
         "source": "hermes-agent repo",
         "docs": "https://hermes-agent.nousresearch.com/docs/reference/model-catalog"
    ```

    ```diff
    @@ -9,7 +9,7 @@
         "openrouter": {
           "metadata": {
             "display_name": "OpenRouter",
    -        "note": "Descriptions drive picker badges. Live /api/v1/models filters curated ids by tool-calling support and free pricing."
    +        "note": "Descriptions drive picker badges. Live /api/v1/models filters curated ids by tool-calling support and free pricing. The entry labeled \"default\": true is the model Hermes silently lands on when the user never picked one."
           },
           "models": [
             {
    ```

    ```diff
    @@ -118,7 +118,8 @@
             },
             {
               "id": "z-ai/glm-5.2",
    -          "description": ""
    +          "description": "default",
    +          "default": true
             },
             {
               "id": "z-ai/glm-5.1",
    ```

    ```diff
    @@ -177,7 +178,7 @@
         "nous": {
           "metadata": {
             "display_name": "Nous Portal",
    -        "note": "Free-tier gating is determined live via Portal pricing (partition_nous_models_by_tier), not this manifest."
    +        "note": "Free-tier gating is determined live via Portal pricing (partition_nous_models_by_tier), not this manifest. The entry labeled \"default\": true is the model Hermes silently lands on when the user never picked one."
           },
           "models": [
             {
    ```

    ```diff
    @@ -256,7 +257,8 @@
               "id": "minimax/minimax-m3"
             },
             {
    -          "id": "z-ai/glm-5.2"
    +          "id": "z-ai/glm-5.2",
    +          "default": true
             },
             {
               "id": "z-ai/glm-5.1"
    ```
- **#59781: fix(models): prefer free Nous silent default**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#64771`

## Fix status

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