mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-09 03:11:58 +00:00
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.
95 lines
3.2 KiB
Python
Executable file
95 lines
3.2 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 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)
|
|
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())
|