feat: add sync_models.py script to auto-update Hermes custom_providers from router model list
This commit is contained in:
parent
95c87a764b
commit
903f06c634
161
scripts/sync_models.py
Normal file
161
scripts/sync_models.py
Normal file
@ -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()
|
||||
Loading…
Reference in New Issue
Block a user