/ scripts / build_model_catalog.py
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())