#!/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()