feat(models): catalog-labeled silent default — GLM-5.2 marked "default": true in the model catalog

The remote model catalog (website/static/api/model-catalog.json) now labels
exactly one entry per provider block with "default": true — z-ai/glm-5.2 for
both OpenRouter and Nous Portal. That labeled entry is the model Hermes
silently lands on when the user never picked one, and it can be rotated by
editing the manifest alone: no release needed.

- model_catalog.py: get_default_model_from_cache() reads the label from the
  in-process/disk cache only — never triggers a network fetch, so hot
  resolution paths (agent build, gateway session setup) stay network-free.
- models.py: get_preferred_silent_default_model() resolves catalog label
  first, PREFERRED_SILENT_DEFAULT_MODEL constant second (offline/fresh
  install). _PROVIDER_SILENT_DEFAULT_OVERRIDES dict replaced by
  _SILENT_DEFAULT_PROVIDERS routing through the shared resolver.
  fetch_openrouter_models() preserves the "default" badge through live
  /v1/models refreshes so the picker shows it.
- scripts/build_model_catalog.py: generator emits the default label so
  regeneration can't drop it.
- website/docs/reference/model-catalog.md: schema documents the new field.
- Salvaged from PR #61141 (@HumphreySun98): bare-provider /model switches
  (/model nous) route through the cost-safe default instead of curated
  entry [0].
- tests: catalog-label precedence, constant fallback, stale-label fallback,
  cache-only (no network) guarantee, and a shipped-manifest contract test
  pinning the labeled entry to PREFERRED_SILENT_DEFAULT_MODEL.

E2E (temp HERMES_HOME): fresh-install constant fallback, shipped-manifest
label read, release-free rotation (relabeled cache -> new default across
models.py, tui_gateway, and gateway empty-model paths) all verified.
This commit is contained in:
Teknium 2026-07-14 21:54:41 -07:00
parent 97375e0f06
commit b34e565957
10 changed files with 275 additions and 76 deletions

View file

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

View file

@ -74,7 +74,7 @@ OPENROUTER_MODELS: list[tuple[str, str]] = [
# 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", ""),
@ -1301,9 +1301,14 @@ _PROVIDER_ALIASES = {
}
# 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
@ -1311,43 +1316,57 @@ _PROVIDER_ALIASES = {
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:
@ -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 ""
@ -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:
@ -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)
@ -1980,10 +2008,10 @@ def detect_static_provider_for_model(
# ``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
# ``_PROVIDER_SILENT_DEFAULT_OVERRIDES`` exists to prevent. For
# providers without an override this is unchanged (it returns
# ``models[0]``).
# 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],

View file

@ -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")
@ -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")

View file

@ -33,12 +33,31 @@ sys.path.insert(0, REPO_ROOT)
# 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,
@ -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
],
},
@ -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", [])
],
},

View file

@ -288,6 +288,68 @@ class TestCuratedAccessors:
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

View file

@ -82,7 +82,10 @@ class TestFetchOpenRouterModels:
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

View file

@ -38,14 +38,14 @@ class TestGetDefaultModelForProvider:
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")
@ -53,27 +53,68 @@ class TestGetDefaultModelForProvider:
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.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="qwen/qwen3.7-plus",
):
result = models_mod.get_default_model_for_provider("openai-codex")
assert result == models_mod._PROVIDER_MODELS["openai-codex"][0]
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(
"hermes_cli.model_catalog.get_default_model_from_cache",
return_value="does-not-exist-model",
):
result = models_mod.get_default_model_for_provider("nous")
assert result == models_mod._PROVIDER_MODELS["nous"][0]
class TestDetectStaticProviderCostSafeDefault:
@ -101,15 +142,15 @@ class TestDetectStaticProviderCostSafeDefault:
assert model != _PROVIDER_MODELS["nous"][0]
def test_provider_without_override_still_uses_first_model(self):
"""Providers with no silent-default override are unchanged."""
"""Providers outside _SILENT_DEFAULT_PROVIDERS are unchanged."""
from hermes_cli.models import (
_PROVIDER_MODELS,
_PROVIDER_SILENT_DEFAULT_OVERRIDES,
_SILENT_DEFAULT_PROVIDERS,
detect_static_provider_for_model,
)
for provider in ("anthropic", "xai"):
if provider in _PROVIDER_SILENT_DEFAULT_OVERRIDES:
if provider in _SILENT_DEFAULT_PROVIDERS:
continue
result = detect_static_provider_for_model(provider, "openrouter")
assert result is not None

View file

@ -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"

View file

@ -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": ""}
]
@ -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"}
]
@ -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

View file

@ -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"
@ -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": [
{
@ -118,7 +118,8 @@
},
{
"id": "z-ai/glm-5.2",
"description": ""
"description": "default",
"default": true
},
{
"id": "z-ai/glm-5.1",
@ -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": [
{
@ -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"