feat(models): remote model catalog manifest for OpenRouter + Nous Portal (#16033)
OpenRouter and Nous Portal curated picker lists now resolve via a JSON
manifest served by the docs site, falling back to the in-repo snapshot
when unreachable. Lets us update model lists without shipping a release.
Live URL: https://hermes-agent.nousresearch.com/docs/api/model-catalog.json
(source at website/static/api/model-catalog.json; auto-deploys via the
existing deploy-site.yml GitHub Pages pipeline on every merge to main).
Schema (v1) carries id + optional description + free-form metadata at
manifest, provider, and model levels. Pricing and context length stay
live-fetched via existing machinery (/v1/models endpoints, models.dev).
Config (new model_catalog section, default enabled):
model_catalog.url master manifest URL
model_catalog.ttl_hours disk cache TTL (default 24h)
model_catalog.providers.<name>.url optional per-provider override
Fetch pipeline: in-process cache -> disk cache (fresh < TTL) -> HTTP
fetch -> disk-cache-on-failure fallback -> in-repo snapshot as last
resort. Never raises to callers; at worst returns the bundled list.
Changes:
- website/static/api/model-catalog.json initial manifest (35 OR + 31 Nous)
- scripts/build_model_catalog.py regenerator from in-repo lists
- hermes_cli/model_catalog.py fetch + validate + cache module
- hermes_cli/models.py fetch_openrouter_models() +
new get_curated_nous_model_ids()
- hermes_cli/main.py, hermes_cli/auth.py Nous flows use the helper
- hermes_cli/config.py model_catalog defaults
- website/docs/reference/model-catalog.md + sidebars.ts
- tests/hermes_cli/test_model_catalog.py 21 tests (validation, fetch
success/failure, accessors,
disabled, overrides, integration)
2026-04-26 05:46:43 -07:00
|
|
|
#!/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 OPENROUTER_MODELS, _PROVIDER_MODELS # noqa: E402
|
|
|
|
|
|
|
|
|
|
OUTPUT_PATH = os.path.join(REPO_ROOT, "website", "static", "api", "model-catalog.json")
|
|
|
|
|
CATALOG_VERSION = 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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."
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
"models": [
|
|
|
|
|
{"id": mid, "description": 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."
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
"models": [
|
|
|
|
|
{"id": mid}
|
|
|
|
|
for mid in _PROVIDER_MODELS.get("nous", [])
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
catalog = build_catalog()
|
|
|
|
|
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
codebase: add encoding='utf-8' to all bare open() calls (PLW1514)
Closes the last Python-on-Windows UTF-8 exposure by making every
text-mode open() call explicit about its encoding.
Before: on Windows, bare open(path, 'r') defaults to the system
locale encoding (cp1252 on US-locale installs). That means reading
any config/yaml/markdown/json file with non-ASCII content either
crashes with UnicodeDecodeError or silently mis-decodes bytes.
After: all 89 affected call sites in production code now pass
encoding='utf-8' explicitly. Works identically on every platform
and every locale, no surprise behavior.
Mechanical sweep via:
ruff check --preview --extend-select PLW1514 --unsafe-fixes --fix --exclude 'tests,venv,.venv,node_modules,website,optional-skills, skills,tinker-atropos,plugins' .
All 89 fixes have the same shape: open(x) or open(x, mode) became
open(x, encoding='utf-8') or open(x, mode, encoding='utf-8'). Nothing
else changed. Every modified file still parses and the Windows/sandbox
test suite is still green (85 passed, 14 skipped, 0 failed across
tests/tools/test_code_execution_windows_env.py +
tests/tools/test_code_execution_modes.py + tests/tools/test_env_passthrough.py +
tests/test_hermes_bootstrap.py).
Scope notes:
- tests/ excluded: test fixtures can use locale encoding intentionally
(exercising edge cases). If we want to tighten tests later that's
a separate PR.
- plugins/ excluded: plugin-specific conventions may differ; plugin
authors own their code.
- optional-skills/ and skills/ excluded: skill scripts are user-authored
and we don't want to mass-edit them.
- website/ and tinker-atropos/ excluded: vendored / generated content.
46 files touched, 89 +/- lines (symmetric replacement). No behavior
change on POSIX or on Windows when the file is ASCII; bug fix on
Windows when the file contains non-ASCII.
2026-05-07 19:24:45 -07:00
|
|
|
with open(OUTPUT_PATH, "w", encoding="utf-8") as fh:
|
feat(models): remote model catalog manifest for OpenRouter + Nous Portal (#16033)
OpenRouter and Nous Portal curated picker lists now resolve via a JSON
manifest served by the docs site, falling back to the in-repo snapshot
when unreachable. Lets us update model lists without shipping a release.
Live URL: https://hermes-agent.nousresearch.com/docs/api/model-catalog.json
(source at website/static/api/model-catalog.json; auto-deploys via the
existing deploy-site.yml GitHub Pages pipeline on every merge to main).
Schema (v1) carries id + optional description + free-form metadata at
manifest, provider, and model levels. Pricing and context length stay
live-fetched via existing machinery (/v1/models endpoints, models.dev).
Config (new model_catalog section, default enabled):
model_catalog.url master manifest URL
model_catalog.ttl_hours disk cache TTL (default 24h)
model_catalog.providers.<name>.url optional per-provider override
Fetch pipeline: in-process cache -> disk cache (fresh < TTL) -> HTTP
fetch -> disk-cache-on-failure fallback -> in-repo snapshot as last
resort. Never raises to callers; at worst returns the bundled list.
Changes:
- website/static/api/model-catalog.json initial manifest (35 OR + 31 Nous)
- scripts/build_model_catalog.py regenerator from in-repo lists
- hermes_cli/model_catalog.py fetch + validate + cache module
- hermes_cli/models.py fetch_openrouter_models() +
new get_curated_nous_model_ids()
- hermes_cli/main.py, hermes_cli/auth.py Nous flows use the helper
- hermes_cli/config.py model_catalog defaults
- website/docs/reference/model-catalog.md + sidebars.ts
- tests/hermes_cli/test_model_catalog.py 21 tests (validation, fetch
success/failure, accessors,
disabled, overrides, integration)
2026-04-26 05:46:43 -07:00
|
|
|
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())
|