mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
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.
118 lines
3.9 KiB
Python
Executable file
118 lines
3.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Build the Hermes Model Catalog — a centralized JSON manifest of curated models.
|
|
|
|
This script reads the in-repo hardcoded curated lists (``OPENROUTER_MODELS``,
|
|
``_PROVIDER_MODELS["nous"]``) and writes them to a JSON manifest that the
|
|
Hermes CLI fetches at runtime. Publishing the catalog through the docs site
|
|
lets maintainers update model lists without shipping a Hermes release.
|
|
|
|
The runtime fetcher falls back to the same in-repo hardcoded lists if the
|
|
manifest is unreachable, so this script is a convenience for keeping the
|
|
manifest in sync — not a source of truth.
|
|
|
|
Usage::
|
|
|
|
python scripts/build_model_catalog.py
|
|
|
|
Output: ``website/static/api/model-catalog.json``
|
|
|
|
Live URL (after ``deploy-site.yml`` runs on merge to main):
|
|
``https://hermes-agent.nousresearch.com/docs/api/model-catalog.json``
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
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 ( # 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,
|
|
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"metadata": {
|
|
"source": "hermes-agent repo",
|
|
"docs": "https://hermes-agent.nousresearch.com/docs/reference/model-catalog",
|
|
},
|
|
"providers": {
|
|
"openrouter": {
|
|
"metadata": {
|
|
"display_name": "OpenRouter",
|
|
"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": [
|
|
_openrouter_entry(mid, desc)
|
|
for mid, desc in OPENROUTER_MODELS
|
|
],
|
|
},
|
|
"nous": {
|
|
"metadata": {
|
|
"display_name": "Nous Portal",
|
|
"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": [
|
|
_nous_entry(mid)
|
|
for mid in _PROVIDER_MODELS.get("nous", [])
|
|
],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
catalog = build_catalog()
|
|
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
|
with open(OUTPUT_PATH, "w", encoding="utf-8") as fh:
|
|
json.dump(catalog, fh, indent=2)
|
|
fh.write("\n")
|
|
|
|
print(f"Wrote {OUTPUT_PATH}")
|
|
for provider, block in catalog["providers"].items():
|
|
print(f" {provider}: {len(block['models'])} models")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|