From 903f06c634fe357c63cfb99e05ae75150dade7c1 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Jun 2026 21:10:36 +0000 Subject: [PATCH] feat: add sync_models.py script to auto-update Hermes custom_providers from router model list --- scripts/sync_models.py | 161 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 scripts/sync_models.py diff --git a/scripts/sync_models.py b/scripts/sync_models.py new file mode 100644 index 0000000..53c252c --- /dev/null +++ b/scripts/sync_models.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Sync intelligence-router model list into Hermes custom_providers. + +Usage: + # One-shot: discover models from the router and update Hermes config + python3 scripts/sync_models.py + + # Cron mode (auto): set up via: + # cp scripts/sync_models.py ~/.hermes/scripts/ + # hermes cron create --schedule "every 30m" --no-agent --script sync_models.py + +Silent exit when nothing changed. Prints a summary + restarts the gateway when +the model list differs. +""" + +import json +import os +import subprocess +import sys +import urllib.error +import urllib.request +from pathlib import Path + +# ── CONFIGURE THESE ────────────────────────────────────────────────── +ROUTER_BASE_URL = "http://10.0.4.100:9001/v1" +PROVIDER_NAME = "intelligence_router" +GATEWAY_SERVICE = "hermes-gateway" +# ───────────────────────────────────────────────────────────────────── + +MODELS_URL = f"{ROUTER_BASE_URL}/models" +CONFIG_PATH = Path(os.path.expanduser("~/.hermes/config.yaml")) + + +def fetch_models() -> list[str] | None: + try: + req = urllib.request.Request(MODELS_URL, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read().decode()) + models = sorted(m["id"] for m in data.get("data", []) if isinstance(m, dict)) + return models if models else None + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as e: + print(f"ERROR: Failed to fetch models from {MODELS_URL}: {e}", file=sys.stderr) + return None + + +def read_current_models() -> list[str]: + """Parse current custom_providers entries for our provider name.""" + if not CONFIG_PATH.exists(): + return [] + + models = [] + with open(CONFIG_PATH) as f: + content = f.read() + + idx = content.find("custom_providers:") + if idx == -1: + return [] + + section = content[idx:] + lines = section.split("\n") + + current_entry = {} + for line in lines: + s = line.strip() + if s.startswith("- base_url:"): + if current_entry.get("name") == PROVIDER_NAME: + m = current_entry.get("model", "") + if m: + models.append(m) + current_entry = {} + elif s.startswith("model:"): + current_entry["model"] = s.split("model:", 1)[1].strip().strip("'\"") + elif s.startswith("name:"): + current_entry["name"] = s.split("name:", 1)[1].strip().strip("'\"") + elif s and not s.startswith(("-", " ")): + break + + # Don't forget the last entry + if current_entry.get("name") == PROVIDER_NAME: + m = current_entry.get("model", "") + if m: + models.append(m) + + return sorted(models) + + +def generate_block(models: list[str]) -> str: + lines = ["custom_providers:"] + for m in models: + lines.append(f"- base_url: {ROUTER_BASE_URL}") + lines.append(f" model: {m}") + lines.append(f" name: {PROVIDER_NAME}") + return "\n".join(lines) + + +def replace_section(models: list[str]) -> bool: + """Replace the custom_providers section in-place. Returns True if changed.""" + if not CONFIG_PATH.exists(): + return False + + import yaml + + content = CONFIG_PATH.read_text() + config = yaml.safe_load(content) + + new_entries = [ + {"base_url": ROUTER_BASE_URL, "model": m, "name": PROVIDER_NAME} + for m in models + ] + + if config.get("custom_providers") == new_entries: + return False + + config["custom_providers"] = new_entries + CONFIG_PATH.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) + return True + + +def restart_gateway() -> bool: + try: + r = subprocess.run( + ["systemctl", "--user", "restart", GATEWAY_SERVICE], + capture_output=True, text=True, timeout=30, + ) + return r.returncode == 0 + except Exception: + return False + + +def main(): + models = fetch_models() + if models is None: + sys.exit(1) + + current = read_current_models() + if current == models: + print("Model list unchanged — nothing to do.") + return + + added = set(models) - set(current) + removed = set(current) - set(models) + print(f"Model list changed! {len(current)} → {len(models)} models") + if added: + print(f" Added: {sorted(added)}") + if removed: + print(f" Removed: {sorted(removed)}") + + if not replace_section(models): + print("ERROR: Config update failed") + return + + print("Config updated. Restarting gateway...") + if restart_gateway(): + print("Gateway restarted successfully.") + else: + print("WARNING: Gateway restart failed — restart manually.") + + +if __name__ == "__main__": + main() \ No newline at end of file