161 lines
4.9 KiB
Python
161 lines
4.9 KiB
Python
|
|
#!/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()
|