File size: 5,646 Bytes
5655a42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """Endpoint resolution for llamacpp-alias requests (provider integration).
``provider: llamacpp`` with no explicit base_url resolves, in order, to the managed server (state
file), a detected external llama-server, or — during a backend boot race — the managed server once
its state file appears.
"""
from __future__ import annotations
from contextlib import suppress
import json
import logging
import threading
import time
import urllib.request
LLAMACPP_ALIASES = frozenset({"llamacpp", "llama.cpp", "llama-cpp"})
logger = logging.getLogger(__name__)
def _pid_alive(pid: int) -> bool:
"""Liveness for the state file's supervisor-child pid: psutil when available, else True
(optimistic). On Windows ``os.kill(pid, 0)`` TERMINATES the process — never use it as a probe."""
if not pid or pid < 0:
return False
with suppress(Exception):
import psutil # type: ignore
return psutil.pid_exists(pid)
return True
def _state_endpoint() -> dict | None:
from hermes_cli.local_runtime.recovery import is_modern, read_state, recorded_process
state = read_state()
base_url = state.get("base_url", "")
if not isinstance(base_url, str) or not base_url:
return None
if is_modern(state):
if recorded_process(state) is None:
return None
else:
# Preserve the legacy endpoint shape, with malformed PID values rejected.
try:
pid = state.get("pid")
if isinstance(pid, bool) or not _pid_alive(int(pid or 0)):
return None
except (TypeError, ValueError, OverflowError):
return None
return {"base_url": base_url, "api_key": state.get("api_key", "")}
def managed_root() -> "tuple[str, str] | None":
"""(base_root, api_key) of the managed router, or None. Resolved through the
ownership-guarded reader, not a raw state-file read: on the shared stable port a foreign
install's server answers /health for anyone, and a raw read would attach callers to someone
else's server."""
with suppress(Exception):
state = _state_endpoint()
if state is None:
return None
base = str(state.get("base_url", "")).rsplit("/v1", 1)[0]
return (base, str(state.get("api_key", ""))) if base else None
return None
def managed_get_json(base: str, api_key: str, route: str, timeout_s: float) -> object:
"""Authenticated GET against the managed router; raises on any transport/decode failure."""
req = urllib.request.Request(f"{base}{route}",
headers={"Authorization": f"Bearer {api_key}"})
with urllib.request.urlopen(req, timeout=timeout_s) as r:
return json.loads(r.read())
def resolve_llamacpp_endpoint(config: dict | None = None,
wait_for_boot_s: float = 8.0) -> dict | None:
"""Managed-first, detection-second endpoint for llamacpp aliases.
Boot-race rung: on a fresh backend start there is NO state file yet — the lifespan boot thread
is still spawning the server (≈1-3 s) while the desktop's readiness probe fires the moment the
WebSocket connects.
"""
managed = _state_endpoint()
if managed:
return managed
from hermes_cli.local_runtime.detect import detect_server
ports = ((config or {}).get("local_runtime") or {}).get("detect_ports") or []
hit = detect_server(extra_ports=tuple(int(p) for p in ports))
if hit and not hit.auth_required:
return {"base_url": hit.base_url, "api_key": ""}
if wait_for_boot_s > 0 and _boot_in_flight(config):
_kick_managed_boot(config)
deadline = time.monotonic() + wait_for_boot_s
while time.monotonic() < deadline:
time.sleep(0.25)
managed = _state_endpoint()
if managed:
return managed
return None
_KICK_LOCK = threading.Lock()
def _load_config_if_none(config: dict | None) -> dict | None:
if config is not None:
return config
from hermes_cli.config import load_config
return load_config()
def _kick_managed_boot(config: dict | None) -> None:
"""Actively start the managed server when resolution finds it missing — the wait loop assumes
some OTHER thread is bringing it up, which is true only at backend start."""
if not _KICK_LOCK.acquire(blocking=False):
return # a kick is already in flight
def _boot() -> None:
try:
from hermes_cli.local_runtime.bootstrap import ensure_local_runtime
ensure_local_runtime(_load_config_if_none(config))
except Exception: # noqa: BLE001 — best-effort; resolution falls back
logger.warning("on-demand managed-server boot failed", exc_info=True)
finally:
_KICK_LOCK.release()
threading.Thread(target=_boot, daemon=True,
name="lr-on-demand-boot").start()
def _boot_in_flight(config: dict | None) -> bool:
"""True when the managed runtime is enabled and installed (a verified-manifest scan under
runtimes_root(), NOT a bare ``server_binary()`` call — that needs an install_dir, and calling
it bare once made this gate throw-and-return False forever, disabling the boot wait)."""
with suppress(Exception):
config = _load_config_if_none(config)
if not ((config or {}).get("local_runtime") or {}).get("enabled"):
return False
from hermes_cli.local_runtime.binaries import manifest_verified, runtimes_root
return any(manifest_verified(m) for m in runtimes_root().glob("*/*/manifest.json"))
return False
|