build_model_catalog.py
1 #!/usr/bin/env python3 2 """Build the Hermes Model Catalog — a centralized JSON manifest of curated models. 3 4 This script reads the in-repo hardcoded curated lists (``OPENROUTER_MODELS``, 5 ``_PROVIDER_MODELS["nous"]``) and writes them to a JSON manifest that the 6 Hermes CLI fetches at runtime. Publishing the catalog through the docs site 7 lets maintainers update model lists without shipping a Hermes release. 8 9 The runtime fetcher falls back to the same in-repo hardcoded lists if the 10 manifest is unreachable, so this script is a convenience for keeping the 11 manifest in sync — not a source of truth. 12 13 Usage:: 14 15 python scripts/build_model_catalog.py 16 17 Output: ``website/static/api/model-catalog.json`` 18 19 Live URL (after ``deploy-site.yml`` runs on merge to main): 20 ``https://hermes-agent.nousresearch.com/docs/api/model-catalog.json`` 21 """ 22 23 from __future__ import annotations 24 25 import json 26 import os 27 import sys 28 from datetime import datetime, timezone 29 30 REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 31 sys.path.insert(0, REPO_ROOT) 32 33 # Ensure HERMES_HOME is set for imports that touch it at module level. 34 os.environ.setdefault("HERMES_HOME", os.path.join(os.path.expanduser("~"), ".hermes")) 35 36 from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS # noqa: E402 37 38 OUTPUT_PATH = os.path.join(REPO_ROOT, "website", "static", "api", "model-catalog.json") 39 CATALOG_VERSION = 1 40 41 42 def build_catalog() -> dict: 43 return { 44 "version": CATALOG_VERSION, 45 "updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), 46 "metadata": { 47 "source": "hermes-agent repo", 48 "docs": "https://hermes-agent.nousresearch.com/docs/reference/model-catalog", 49 }, 50 "providers": { 51 "openrouter": { 52 "metadata": { 53 "display_name": "OpenRouter", 54 "note": ( 55 "Descriptions drive picker badges. Live /api/v1/models " 56 "filters curated ids by tool-calling support and free pricing." 57 ), 58 }, 59 "models": [ 60 {"id": mid, "description": desc} 61 for mid, desc in OPENROUTER_MODELS 62 ], 63 }, 64 "nous": { 65 "metadata": { 66 "display_name": "Nous Portal", 67 "note": ( 68 "Free-tier gating is determined live via Portal pricing " 69 "(partition_nous_models_by_tier), not this manifest." 70 ), 71 }, 72 "models": [ 73 {"id": mid} 74 for mid in _PROVIDER_MODELS.get("nous", []) 75 ], 76 }, 77 }, 78 } 79 80 81 def main() -> int: 82 catalog = build_catalog() 83 os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) 84 with open(OUTPUT_PATH, "w") as fh: 85 json.dump(catalog, fh, indent=2) 86 fh.write("\n") 87 88 print(f"Wrote {OUTPUT_PATH}") 89 for provider, block in catalog["providers"].items(): 90 print(f" {provider}: {len(block['models'])} models") 91 return 0 92 93 94 if __name__ == "__main__": 95 sys.exit(main())