{"repo":"NousResearch/hermes-agent","public_id":"58364","display_label":"Silent default model selection and cost-safety concerns","summary":"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.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":64771,"merged_at":"2026-07-15T07:10:32+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-15T07:10:32+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":58364,"pr_number":64771,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-15T07:10:32+00:00"}],"members":[{"kind":"issue","number":58364,"title":"Silent default for nous provider pins deepseek/deepseek-v4-flash, which is no longer free — the cost-safety guard now bills users","url":null,"state":"closed"},{"kind":"pr","number":59781,"title":"fix(models): prefer free Nous silent default","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64771","number":64771}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":64771,"title":"feat(models): catalog-labeled silent default (GLM-5.2) + bare-provider /model cost-safe routing (#61141 salvage)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"hermes_cli/model_catalog.py","hunks":["@@ -356,6 +356,42 @@ def get_curated_nous_models() -> list[str] | None:\n     return out or None\n \n \n+def _default_model_from_block(block: dict[str, Any] | None) -> str | None:\n+    \"\"\"Return the id of the model entry labeled ``\"default\": true``, or None.\"\"\"\n+    if not isinstance(block, dict):\n+        return None\n+    for m in block.get(\"models\", []):\n+        if isinstance(m, dict) and m.get(\"default\"):\n+            mid = str(m.get(\"id\") or \"\").strip()\n+            if mid:\n+                return mid\n+    return None\n+\n+\n+def get_default_model_from_cache(provider: str) -> str | None:\n+    \"\"\"Return the catalog's labeled default model for ``provider`` — cache only.\n+\n+    The manifest marks exactly one model entry per provider with\n+    ``\"default\": true``; that entry is the model Hermes silently lands on when\n+    the user never picked one. This accessor reads ONLY the in-process copy or\n+    the disk cache — it NEVER triggers a network fetch, so it is safe on hot\n+    resolution paths (agent build, gateway session setup) that must stay\n+    network-free. The cache is kept fresh by the picker/`hermes update` paths;\n+    when no cached manifest exists (fresh install, offline), returns None and\n+    the caller falls back to the in-repo constant.\n+    \"\"\"\n+    if _catalog_cache is not None:\n+        block = _catalog_cache.get(\"providers\", {}).get(provider)\n+        found = _default_model_from_block(block)\n+        if found:\n+            return found\n+    disk_data, _mtime = _read_disk_cache()\n+    if disk_data is not None:\n+        block = disk_data.get(\"providers\", {}).get(provider)\n+        return _default_model_from_block(block)\n+    return None\n+\n+\n def seed_cache_from_checkout(project_root: \"Path | str\") -> bool:\n     \"\"\"Overwrite the disk cache with the catalog shipped in a local checkout.\n "]},{"file":"hermes_cli/models.py","hunks":["@@ -74,7 +74,7 @@ def _urlopen_model_catalog_request(req: urllib.request.Request, *, timeout: floa\n     # MiniMax\n     (\"minimax/minimax-m3\",                     \"\"),\n     # Z-AI\n-    (\"z-ai/glm-5.2\",                           \"\"),\n+    (\"z-ai/glm-5.2\",                           \"default\"),\n     (\"z-ai/glm-5.1\",                           \"\"),\n     # Xiaomi\n     (\"xiaomi/mimo-v2.5-pro\",                   \"\"),","@@ -1301,9 +1301,14 @@ def group_providers(slugs):\n }\n \n \n-# The model Hermes silently lands on when the user never picked one and the\n-# provider's catalog carries it (GUI onboarding confirm card, empty\n-# ``model.default``, provider-set-but-model-missing resolution). Deliberately a\n+# In-repo fallback for the model Hermes silently lands on when the user never\n+# picked one (GUI onboarding confirm card, empty ``model.default``,\n+# provider-set-but-model-missing resolution). The AUTHORITATIVE source is the\n+# remote model catalog: the manifest labels exactly one entry per provider\n+# with ``\"default\": true`` (see get_default_model_from_cache in\n+# model_catalog.py), so maintainers can rotate the default without shipping a\n+# release. This constant is the offline/fresh-install fallback and MUST match\n+# the labeled entry in website/static/api/model-catalog.json. Deliberately a\n # capable low-cost model rather than the curated lists' entry [0]: aggregator\n # lists are ordered most-capable-first, so [0] is the priciest Anthropic\n # flagship (claude-fable-5 / opus) — silently billing the most expensive model","@@ -1311,43 +1316,57 @@ def group_providers(slugs):\n PREFERRED_SILENT_DEFAULT_MODEL = \"z-ai/glm-5.2\"\n \n \n-def pick_silent_default_model(model_ids: list[str]) -> str:\n+def get_preferred_silent_default_model(provider: str = \"openrouter\") -> str:\n+    \"\"\"Return the silent-default model id — catalog label first, constant second.\n+\n+    Reads the ``\"default\": true`` label from the cached remote catalog\n+    (never hits the network — safe on hot resolution paths), falling back to\n+    :data:`PREFERRED_SILENT_DEFAULT_MODEL` when no cached manifest exists or\n+    the provider block carries no label.\n+    \"\"\"\n+    try:\n+        from hermes_cli.model_catalog import get_default_model_from_cache\n+        labeled = get_default_model_from_cache(provider)\n+        if labeled:\n+            return labeled\n+    except Exception:\n+        pass\n+    return PREFERRED_SILENT_DEFAULT_MODEL\n+\n+\n+def pick_silent_default_model(model_ids: list[str], provider: str = \"openrouter\") -> str:\n     \"\"\"Pick the silent default from an available-models list.\n \n-    Returns :data:`PREFERRED_SILENT_DEFAULT_MODEL` when the list carries it,\n+    Returns the catalog-labeled default (see\n+    :func:`get_preferred_silent_default_model`) when the list carries it,\n     else the first entry, else \"\". Used by every surface that must choose a\n     model on the user's behalf without an interactive picker (GUI onboarding\n     recommended-default, empty-model runtime fallback).\n     \"\"\"\n-    if PREFERRED_SILENT_DEFAULT_MODEL in model_ids:\n-        return PREFERRED_SILENT_DEFAULT_MODEL\n+    preferred = get_preferred_silent_default_model(provider)\n+    if preferred in model_ids:\n+        return preferred\n     return model_ids[0] if model_ids else \"\"\n \n \n-# Cost-safe overrides for the *silent* auto-default\n-# (``get_default_model_for_provider``). Most providers' curated lists lead with a\n-# sensible default, but metered aggregators (Nous Portal, OpenRouter) order\n+# Providers whose *silent* auto-default must go through the cost-safe\n+# catalog-labeled default (``get_preferred_silent_default_model``) instead of\n+# curated-list entry [0]. Metered aggregators (Nous Portal, OpenRouter) order\n # their lists best-/most-capable-first — entry [0] is the priciest flagship\n # (``anthropic/claude-fable-5``). Using that as the non-interactive fallback\n # when a profile sets a provider with no model silently bills the most\n # expensive model for traffic the user never opted into (a missing default\n-# escalated to Opus and billed 863 requests before the user noticed). Pin the\n-# silent default to ``PREFERRED_SILENT_DEFAULT_MODEL`` instead so a missing\n-# model can never escalate to the flagship.\n+# escalated to Opus and billed 863 requests before the user noticed). The\n+# catalog manifest labels the default entry (``\"default\": true``) so it can\n+# rotate without a release; a missing model must never escalate to the\n+# flagship.\n #\n-# This is deliberately a fixed, side-effect-free default for the hot resolution\n-# path. The *interactive* default (GUI onboarding / ``hermes model``) uses the\n-# richer free/paid-tier-aware resolver — see ``get_recommended_default_model``\n-# in hermes_cli/web_server.py and ``partition_nous_models_by_tier`` — which can\n-# hit the Portal; this fallback must stay cheap and network-free.\n-_PROVIDER_SILENT_DEFAULT_OVERRIDES: dict[str, str] = {\n-    \"nous\": PREFERRED_SILENT_DEFAULT_MODEL,\n-    # OpenRouter has no static ``_PROVIDER_MODELS`` entry (its picker list is\n-    # fetched live), but the curated snapshot (``OPENROUTER_MODELS``) carries\n-    # the preferred default — trust the override so provider-set-but-model-\n-    # missing setups land on it instead of resolving to \"\".\n-    \"openrouter\": PREFERRED_SILENT_DEFAULT_MODEL,\n-}\n+# This is deliberately a network-free lookup for the hot resolution path\n+# (cache-only catalog read). The *interactive* default (GUI onboarding /\n+# ``hermes model``) uses the richer free/paid-tier-aware resolver — see\n+# ``get_recommended_default_model`` in hermes_cli/web_server.py and\n+# ``partition_nous_models_by_tier`` — which can hit the Portal.\n+_SILENT_DEFAULT_PROVIDERS: frozenset[str] = frozenset({\"nous\", \"openrouter\"})\n \n \n def get_default_model_for_provider(provider: str) -> str:","@@ -1360,17 +1379,19 @@ def get_default_model_for_provider(provider: str) -> str:\n     For most providers this is the first entry in ``_PROVIDER_MODELS`` — the\n     same model the ``hermes model`` picker offers first. For metered aggregators\n     whose curated list is ordered most-capable-first, that entry is also the\n-    most EXPENSIVE one, so silently defaulting to it is a billing footgun. Such\n-    providers carry an explicit override in\n-    ``_PROVIDER_SILENT_DEFAULT_OVERRIDES``; a missing model must never\n-    auto-escalate to the flagship.\n+    most EXPENSIVE one, so silently defaulting to it is a billing footgun.\n+    Those providers (``_SILENT_DEFAULT_PROVIDERS``) resolve through the\n+    catalog-labeled default instead; a missing model must never auto-escalate\n+    to the flagship.\n     \"\"\"\n     models = _PROVIDER_MODELS.get(provider, [])\n-    override = _PROVIDER_SILENT_DEFAULT_OVERRIDES.get(provider)\n-    # Trust the override when the provider has no static catalog (OpenRouter's\n-    # picker list is fetched live; its curated snapshot carries the override).\n-    if override and (override in models or not models):\n-        return override\n+    if provider in _SILENT_DEFAULT_PROVIDERS:\n+        preferred = get_preferred_silent_default_model(provider)\n+        # Trust the preferred default even when the provider has no static\n+        # catalog (OpenRouter's picker list is fetched live; its curated\n+        # snapshot carries the default).\n+        if preferred and (preferred in models or not models):\n+            return preferred\n     return models[0] if models else \"\"\n \n ","@@ -1456,6 +1477,7 @@ def fetch_openrouter_models(\n         live_by_id[mid] = item\n \n     curated: list[tuple[str, str]] = []\n+    silent_default = get_preferred_silent_default_model(\"openrouter\")\n     for preferred_id in preferred_ids:\n         live_item = live_by_id.get(preferred_id)\n         if live_item is None:","@@ -1465,14 +1487,20 @@ def fetch_openrouter_models(\n         # when the user selects them. Ported from Kilo-Org/kilocode#9068.\n         if not _openrouter_model_supports_tools(live_item):\n             continue\n-        desc = \"free\" if _openrouter_model_is_free(live_item.get(\"pricing\")) else \"\"\n+        if preferred_id == silent_default:\n+            # Keep the silent-default badge through the live refresh so the\n+            # picker shows which model Hermes lands on when none is selected.\n+            desc = \"default\"\n+        else:\n+            desc = \"free\" if _openrouter_model_is_free(live_item.get(\"pricing\")) else \"\"\n         curated.append((preferred_id, desc))\n \n     if not curated:\n         return list(_openrouter_catalog_cache or fallback)\n \n-    first_id, _ = curated[0]\n-    curated[0] = (first_id, \"recommended\")\n+    first_id, first_desc = curated[0]\n+    if not first_desc:\n+        curated[0] = (first_id, \"recommended\")\n     _openrouter_catalog_cache = curated\n     return list(curated)\n ","@@ -1976,7 +2004,18 @@ def detect_static_provider_for_model(\n             and default_models\n             and resolved_provider not in current_keys\n         ):\n-            return (resolved_provider, default_models[0])\n+            # Route through the cost-safe default rather than picking\n+            # ``default_models[0]`` directly. For metered aggregators whose\n+            # curated list is ordered most-capable-first (e.g. Nous Portal),\n+            # entry [0] is the priciest flagship, and typing ``/model nous``\n+            # would silently escalate to it — the exact billing footgun the\n+            # catalog-labeled silent default (``_SILENT_DEFAULT_PROVIDERS``)\n+            # exists to prevent. For providers outside that set this is\n+            # unchanged (it returns ``models[0]``).\n+            return (\n+                resolved_provider,\n+                get_default_model_for_provider(resolved_provider) or default_models[0],\n+            )\n \n     # Aggregators list other providers' models — never auto-switch TO them\n     # If the model belongs to the current provider's catalog, don't suggest switching"]},{"file":"hermes_cli/web_server.py","hunks":["@@ -5605,7 +5605,7 @@ def get_recommended_default_model(provider: str = \"\"):\n                     model_ids, pricing, portal_url\n                 )\n \n-            model = pick_silent_default_model(model_ids)\n+            model = pick_silent_default_model(model_ids, provider=\"nous\")\n             return {\"provider\": \"nous\", \"model\": model, \"free_tier\": bool(free_tier)}\n         except Exception:\n             _log.exception(\"GET /api/model/recommended-default (nous) failed\")","@@ -5623,7 +5623,7 @@ def get_recommended_default_model(provider: str = \"\"):\n         for row in payload.get(\"providers\", []):\n             if str(row.get(\"slug\", \"\")).lower() == slug:\n                 models = [str(m) for m in (row.get(\"models\") or [])]\n-                return {\"provider\": slug, \"model\": pick_silent_default_model(models), \"free_tier\": None}\n+                return {\"provider\": slug, \"model\": pick_silent_default_model(models, provider=slug), \"free_tier\": None}\n         return {\"provider\": slug, \"model\": \"\", \"free_tier\": None}\n     except Exception:\n         _log.exception(\"GET /api/model/recommended-default failed\")"]},{"file":"scripts/build_model_catalog.py","hunks":["@@ -33,12 +33,31 @@\n # Ensure HERMES_HOME is set for imports that touch it at module level.\n os.environ.setdefault(\"HERMES_HOME\", os.path.join(os.path.expanduser(\"~\"), \".hermes\"))\n \n-from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS  # noqa: E402\n+from hermes_cli.models import (  # noqa: E402\n+    OPENROUTER_MODELS,\n+    PREFERRED_SILENT_DEFAULT_MODEL,\n+    _PROVIDER_MODELS,\n+)\n \n OUTPUT_PATH = os.path.join(REPO_ROOT, \"website\", \"static\", \"api\", \"model-catalog.json\")\n CATALOG_VERSION = 1\n \n \n+def _openrouter_entry(mid: str, desc: str) -> dict:\n+    entry: dict = {\"id\": mid, \"description\": desc}\n+    if mid == PREFERRED_SILENT_DEFAULT_MODEL:\n+        entry[\"description\"] = desc or \"default\"\n+        entry[\"default\"] = True\n+    return entry\n+\n+\n+def _nous_entry(mid: str) -> dict:\n+    entry: dict = {\"id\": mid}\n+    if mid == PREFERRED_SILENT_DEFAULT_MODEL:\n+        entry[\"default\"] = True\n+    return entry\n+\n+\n def build_catalog() -> dict:\n     return {\n         \"version\": CATALOG_VERSION,","@@ -53,11 +72,13 @@ def build_catalog() -> dict:\n                     \"display_name\": \"OpenRouter\",\n                     \"note\": (\n                         \"Descriptions drive picker badges. Live /api/v1/models \"\n-                        \"filters curated ids by tool-calling support and free pricing.\"\n+                        \"filters curated ids by tool-calling support and free pricing. \"\n+                        'The entry labeled \"default\": true is the model Hermes '\n+                        \"silently lands on when the user never picked one.\"\n                     ),\n                 },\n                 \"models\": [\n-                    {\"id\": mid, \"description\": desc}\n+                    _openrouter_entry(mid, desc)\n                     for mid, desc in OPENROUTER_MODELS\n                 ],\n             },","@@ -66,11 +87,13 @@ def build_catalog() -> dict:\n                     \"display_name\": \"Nous Portal\",\n                     \"note\": (\n                         \"Free-tier gating is determined live via Portal pricing \"\n-                        \"(partition_nous_models_by_tier), not this manifest.\"\n+                        \"(partition_nous_models_by_tier), not this manifest. \"\n+                        'The entry labeled \"default\": true is the model Hermes '\n+                        \"silently lands on when the user never picked one.\"\n                     ),\n                 },\n                 \"models\": [\n-                    {\"id\": mid}\n+                    _nous_entry(mid)\n                     for mid in _PROVIDER_MODELS.get(\"nous\", [])\n                 ],\n             },"]},{"file":"tests/hermes_cli/test_model_catalog.py","hunks":["@@ -288,6 +288,68 @@ def test_nous_returns_none_when_catalog_empty(self, isolated_home):\n             assert model_catalog.get_curated_nous_models() is None\n \n \n+class TestDefaultModelFromCache:\n+    \"\"\"get_default_model_from_cache reads the '\"default\": true' label without\n+    ever hitting the network.\"\"\"\n+\n+    def _manifest_with_default(self) -> dict:\n+        m = _valid_manifest()\n+        m[\"providers\"][\"openrouter\"][\"models\"][1][\"default\"] = True  # gpt-5.4\n+        m[\"providers\"][\"nous\"][\"models\"][1][\"default\"] = True  # kimi-k2.6\n+        return m\n+\n+    def test_reads_label_from_disk_cache(self, isolated_home):\n+        from hermes_cli import model_catalog\n+        cache = isolated_home / \"cache\"\n+        cache.mkdir()\n+        (cache / \"model_catalog.json\").write_text(\n+            json.dumps(self._manifest_with_default())\n+        )\n+        with patch.object(model_catalog, \"_fetch_manifest\") as fetch:\n+            assert (\n+                model_catalog.get_default_model_from_cache(\"openrouter\")\n+                == \"openai/gpt-5.4\"\n+            )\n+            assert (\n+                model_catalog.get_default_model_from_cache(\"nous\")\n+                == \"moonshotai/kimi-k2.6\"\n+            )\n+            fetch.assert_not_called()\n+\n+    def test_no_label_returns_none(self, isolated_home):\n+        from hermes_cli import model_catalog\n+        cache = isolated_home / \"cache\"\n+        cache.mkdir()\n+        (cache / \"model_catalog.json\").write_text(json.dumps(_valid_manifest()))\n+        with patch.object(model_catalog, \"_fetch_manifest\") as fetch:\n+            assert model_catalog.get_default_model_from_cache(\"openrouter\") is None\n+            fetch.assert_not_called()\n+\n+    def test_no_cache_returns_none_without_network(self, isolated_home):\n+        from hermes_cli import model_catalog\n+        with patch.object(model_catalog, \"_fetch_manifest\") as fetch:\n+            assert model_catalog.get_default_model_from_cache(\"openrouter\") is None\n+            fetch.assert_not_called()\n+\n+    def test_shipped_manifest_labels_glm52_default(self, isolated_home):\n+        \"\"\"Contract with the in-repo manifest: both provider blocks label the\n+        same default entry the code constant points at.\"\"\"\n+        import hermes_cli.model_catalog as model_catalog\n+        from hermes_cli.models import PREFERRED_SILENT_DEFAULT_MODEL\n+\n+        repo_root = Path(model_catalog.__file__).resolve().parent.parent\n+        manifest = json.loads(\n+            (repo_root / \"website\" / \"static\" / \"api\" / \"model-catalog.json\").read_text()\n+        )\n+        for provider in (\"openrouter\", \"nous\"):\n+            block = manifest[\"providers\"][provider]\n+            labeled = [m[\"id\"] for m in block[\"models\"] if m.get(\"default\")]\n+            assert labeled == [PREFERRED_SILENT_DEFAULT_MODEL], (\n+                f\"{provider}: exactly one entry must be labeled default and it \"\n+                f\"must match PREFERRED_SILENT_DEFAULT_MODEL\"\n+            )\n+\n+\n class TestDisabled:\n     def test_disabled_config_short_circuits(self, isolated_home):\n         from hermes_cli import model_catalog"]},{"file":"tests/hermes_cli/test_models.py","hunks":["@@ -82,7 +82,10 @@ def read(self):\n \n     def test_falls_back_to_static_snapshot_on_fetch_failure(self, monkeypatch):\n         monkeypatch.setattr(_models_mod, \"_openrouter_catalog_cache\", None)\n-        with patch(\"hermes_cli.models._urlopen_model_catalog_request\", side_effect=OSError(\"boom\")):\n+        # Pin the remote manifest out too — otherwise the fallback silently\n+        # depends on whatever the deployed catalog currently contains.\n+        with patch(\"hermes_cli.model_catalog.get_curated_openrouter_models\", return_value=None), \\\n+             patch(\"hermes_cli.models._urlopen_model_catalog_request\", side_effect=OSError(\"boom\")):\n             models = fetch_openrouter_models(force_refresh=True)\n \n         assert models == OPENROUTER_MODELS"]},{"file":"tests/test_empty_model_fallback.py","hunks":["@@ -38,14 +38,14 @@ def test_custom_provider_returns_empty(self):\n     def test_nous_silent_default_is_not_the_expensive_flagship(self):\n         \"\"\"Nous Portal is a metered aggregator whose curated list is ordered\n         most-capable-first, so entry [0] is the priciest flagship\n-        (anthropic/claude-opus-4.8). The silent fallback (provider set, no model)\n+        (anthropic/claude-fable-5). The silent fallback (provider set, no model)\n         must NOT escalate to it — otherwise an unconfigured profile silently\n         bills the most expensive model. Regression for the billing footgun.\n         \"\"\"\n         from hermes_cli.models import (\n             _PROVIDER_MODELS,\n-            _PROVIDER_SILENT_DEFAULT_OVERRIDES,\n             get_default_model_for_provider,\n+            get_preferred_silent_default_model,\n         )\n \n         result = get_default_model_for_provider(\"nous\")","@@ -53,27 +53,108 @@ def test_nous_silent_default_is_not_the_expensive_flagship(self):\n         assert \"opus\" not in result.lower(), (\n             f\"silent default escalated to an expensive flagship: {result!r}\"\n         )\n+        assert \"claude\" not in result.lower(), (\n+            f\"silent default escalated to an expensive flagship: {result!r}\"\n+        )\n         assert result != _PROVIDER_MODELS[\"nous\"][0], (\n             \"silent default must not be the most-capable/priciest catalog entry\"\n         )\n-        # The override must point at a model that actually exists in the catalog.\n-        assert result == _PROVIDER_SILENT_DEFAULT_OVERRIDES[\"nous\"]\n+        # The default must resolve through the catalog-label helper and point\n+        # at a model that actually exists in the curated catalog.\n+        assert result == get_preferred_silent_default_model(\"nous\")\n         assert result in _PROVIDER_MODELS[\"nous\"]\n \n-    def test_override_falls_back_to_catalog_when_missing(self):\n-        \"\"\"If an override model is no longer in the catalog, fall back to [0]\n-        rather than returning a stale/absent id.\"\"\"\n+    def test_catalog_label_overrides_constant(self):\n+        \"\"\"A ``\"default\": true`` label in the cached catalog manifest wins over\n+        the in-repo constant, so maintainers can rotate the silent default\n+        without shipping a release.\"\"\"\n+        from unittest.mock import patch\n+\n+        from hermes_cli import models as models_mod\n+\n+        with patch(\n+            \"hermes_cli.model_catalog.get_default_model_from_cache\",\n+            return_value=\"qwen/qwen3.7-plus\",\n+        ):\n+            assert (\n+                models_mod.get_preferred_silent_default_model(\"nous\")\n+                == \"qwen/qwen3.7-plus\"\n+            )\n+            # nous catalog carries qwen3.7-plus, so the full resolver follows.\n+            assert (\n+                models_mod.get_default_model_for_provider(\"nous\")\n+                == \"qwen/qwen3.7-plus\"\n+            )\n+\n+    def test_no_catalog_cache_falls_back_to_constant(self):\n+        \"\"\"With no cached manifest (fresh install / offline), the in-repo\n+        constant is the silent default.\"\"\"\n+        from unittest.mock import patch\n+\n+        from hermes_cli import models as models_mod\n+\n+        with patch(\n+            \"hermes_cli.model_catalog.get_default_model_from_cache\",\n+            return_value=None,\n+        ):\n+            assert (\n+                models_mod.get_preferred_silent_default_model(\"openrouter\")\n+                == models_mod.PREFERRED_SILENT_DEFAULT_MODEL\n+            )\n+\n+    def test_stale_label_not_in_catalog_falls_back(self):\n+        \"\"\"If the labeled default model is no longer in the provider's curated\n+        catalog, fall back to entry [0] rather than returning an absent id.\"\"\"\n         from unittest.mock import patch\n \n         from hermes_cli import models as models_mod\n \n-        with patch.dict(\n-            models_mod._PROVIDER_SILENT_DEFAULT_OVERRIDES,\n-            {\"openai-codex\": \"does-not-exist-model\"},\n-            clear=False,\n+        with patch(\n+            \"hermes_cli.model_catalog.get_default_model_from_cache\",\n+            return_value=\"does-not-exist-model\",\n         ):\n-            result = models_mod.get_default_model_for_provider(\"openai-codex\")\n-            assert result == models_mod._PROVIDER_MODELS[\"openai-codex\"][0]\n+            result = models_mod.get_default_model_for_provider(\"nous\")\n+            assert result == models_mod._PROVIDER_MODELS[\"nous\"][0]\n+\n+\n+class TestDetectStaticProviderCostSafeDefault:\n+    \"\"\"detect_static_provider_for_model must apply the same cost-safe default\n+    as get_default_model_for_provider when a bare provider name is typed as a\n+    model (e.g. ``/model nous``).\"\"\"\n+\n+    def test_bare_nous_does_not_escalate_to_flagship(self):\n+        from hermes_cli.models import (\n+            _PROVIDER_MODELS,\n+            get_default_model_for_provider,\n+            detect_static_provider_for_model,\n+        )\n+\n+        result = detect_static_provider_for_model(\"nous\", \"openrouter\")\n+        assert result is not None\n+        provider, model = result\n+        assert provider == \"nous\"\n+        # Must match the cost-safe silent default, NOT the priciest catalog\n+        # entry [0]. Regression: this path returned _PROVIDER_MODELS[\"nous\"][0]\n+        # directly, re-introducing the billing footgun on the interactive\n+        # ``/model nous`` path.\n+        assert model == get_default_model_for_provider(\"nous\")\n+        assert \"opus\" not in model.lower()\n+        assert model != _PROVIDER_MODELS[\"nous\"][0]\n+\n+    def test_provider_without_override_still_uses_first_model(self):\n+        \"\"\"Providers outside _SILENT_DEFAULT_PROVIDERS are unchanged.\"\"\"\n+        from hermes_cli.models import (\n+            _PROVIDER_MODELS,\n+            _SILENT_DEFAULT_PROVIDERS,\n+            detect_static_provider_for_model,\n+        )\n+\n+        for provider in (\"anthropic\", \"xai\"):\n+            if provider in _SILENT_DEFAULT_PROVIDERS:\n+                continue\n+            result = detect_static_provider_for_model(provider, \"openrouter\")\n+            assert result is not None\n+            assert result[1] == _PROVIDER_MODELS[provider][0]\n \n \n class TestGatewayEmptyModelFallback:"]},{"file":"tui_gateway/server.py","hunks":["@@ -2118,11 +2118,12 @@ def _resolve_model() -> str:\n     if isinstance(m, str) and m:\n         return m.strip()\n     # No env seed and no config preference: fall back to the cost-safe silent\n-    # default, never an expensive Anthropic flagship the user didn't pick.\n+    # default (catalog-labeled, cache-only read), never an expensive Anthropic\n+    # flagship the user didn't pick.\n     try:\n-        from hermes_cli.models import PREFERRED_SILENT_DEFAULT_MODEL\n+        from hermes_cli.models import get_preferred_silent_default_model\n \n-        return PREFERRED_SILENT_DEFAULT_MODEL\n+        return get_preferred_silent_default_model()\n     except Exception:\n         return \"z-ai/glm-5.2\"\n "]},{"file":"website/docs/reference/model-catalog.md","hunks":["@@ -29,6 +29,7 @@ Published on every merge to `main` via the existing `deploy-site.yml` GitHub Pag\n     \"openrouter\": {\n       \"metadata\": {},\n       \"models\": [\n+        {\"id\": \"z-ai/glm-5.2\",         \"description\": \"default\", \"default\": true},\n         {\"id\": \"moonshotai/kimi-k2.6\", \"description\": \"recommended\", \"metadata\": {}},\n         {\"id\": \"openai/gpt-5.4\",       \"description\": \"\"}\n       ]","@@ -36,6 +37,7 @@ Published on every merge to `main` via the existing `deploy-site.yml` GitHub Pag\n     \"nous\": {\n       \"metadata\": {},\n       \"models\": [\n+        {\"id\": \"z-ai/glm-5.2\", \"default\": true},\n         {\"id\": \"anthropic/claude-opus-4.7\"},\n         {\"id\": \"moonshotai/kimi-k2.6\"}\n       ]","@@ -48,7 +50,8 @@ Field notes:\n \n - **`version`** — integer schema version. Future schemas bump this; Hermes refuses manifests with versions it doesn't understand and falls back to the hardcoded snapshot.\n - **`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.\n-- **`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.\n+- **`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.\n+- **`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.\n - **Pricing and context length** are NOT in the manifest. Those come from live provider APIs (`/v1/models` endpoints, models.dev) at fetch time.\n \n ## Fetch behavior"]},{"file":"website/static/api/model-catalog.json","hunks":["@@ -1,6 +1,6 @@\n {\n   \"version\": 1,\n-  \"updated_at\": \"2026-07-09T18:38:59Z\",\n+  \"updated_at\": \"2026-07-15T04:51:12Z\",\n   \"metadata\": {\n     \"source\": \"hermes-agent repo\",\n     \"docs\": \"https://hermes-agent.nousresearch.com/docs/reference/model-catalog\"","@@ -9,7 +9,7 @@\n     \"openrouter\": {\n       \"metadata\": {\n         \"display_name\": \"OpenRouter\",\n-        \"note\": \"Descriptions drive picker badges. Live /api/v1/models filters curated ids by tool-calling support and free pricing.\"\n+        \"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.\"\n       },\n       \"models\": [\n         {","@@ -118,7 +118,8 @@\n         },\n         {\n           \"id\": \"z-ai/glm-5.2\",\n-          \"description\": \"\"\n+          \"description\": \"default\",\n+          \"default\": true\n         },\n         {\n           \"id\": \"z-ai/glm-5.1\",","@@ -177,7 +178,7 @@\n     \"nous\": {\n       \"metadata\": {\n         \"display_name\": \"Nous Portal\",\n-        \"note\": \"Free-tier gating is determined live via Portal pricing (partition_nous_models_by_tier), not this manifest.\"\n+        \"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.\"\n       },\n       \"models\": [\n         {","@@ -256,7 +257,8 @@\n           \"id\": \"minimax/minimax-m3\"\n         },\n         {\n-          \"id\": \"z-ai/glm-5.2\"\n+          \"id\": \"z-ai/glm-5.2\",\n+          \"default\": true\n         },\n         {\n           \"id\": \"z-ai/glm-5.1\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}