Private
Public Access
0
0

feat(api_hooks): add /api/warmup_status and /api/warmup_wait endpoints (sub-track 3)

Sub-track 3 of startup_speedup_20260606. Builds on the Phase 7 minimal
work at b464d1fe which only added warmup_status to /api/gui/diagnostics.

New dedicated endpoints:
- GET /api/warmup_status -> controller.warmup_status() (cheap, lock-guarded)
- GET /api/warmup_wait?timeout=N -> controller.wait_for_warmup(timeout)
  then returns the final status. Default 30s.

Both callable from external clients via ApiHookClient.get_warmup_status()
and ApiHookClient.get_warmup_wait(timeout=30.0).

7 new tests in tests/test_api_hooks_warmup.py (5 unit + 2 live_gui).
All 7 pass.
This commit is contained in:
2026-06-06 21:01:56 -04:00
parent 0f74705d01
commit 8fea8fe9a0
3 changed files with 146 additions and 0 deletions
+17
View File
@@ -301,6 +301,23 @@ class ApiHookClient:
"""
return self._make_request('GET', '/api/performance') or {}
def get_warmup_status(self) -> dict[str, Any]:
"""
Returns the current warmup status: {pending, completed, failed}.
[C: tests/test_api_hooks_warmup.py:test_get_warmup_status_calls_correct_endpoint, tests/test_api_hooks_warmup.py:test_get_warmup_status_handles_empty_response, tests/test_api_hooks_warmup.py:test_live_warmup_status_endpoint]
"""
return self._make_request('GET', '/api/warmup_status') or {}
def get_warmup_wait(self, timeout: float = 30.0) -> dict[str, Any]:
"""
Blocks server-side up to `timeout` seconds waiting for the warmup to
complete, then returns the final status. Useful for external clients
that need to wait until the system is fully ready before issuing AI
requests.
[C: tests/test_api_hooks_warmup.py:test_get_warmup_wait_passes_timeout_as_query_string, tests/test_api_hooks_warmup.py:test_get_warmup_wait_uses_default_timeout_when_unspecified, tests/test_api_hooks_warmup.py:test_get_warmup_wait_handles_empty_response, tests/test_api_hooks_warmup.py:test_live_warmup_wait_endpoint_completes]
"""
return self._make_request('GET', f'/api/warmup_wait?timeout={timeout}') or {}
#endregion: Diagnostics
#region: Project
+43
View File
@@ -321,6 +321,49 @@ class HookHandler(BaseHTTPRequestHandler):
queue = _get_app_attr(app, "_api_event_queue")
if queue: queue_size = len(queue)
self.wfile.write(json.dumps({"threads": threads, "event_queue_size": queue_size}).encode("utf-8"))
elif self.path == "/api/warmup_status" or self.path.startswith("/api/warmup_status?"):
# Cheap snapshot of the AppController's warmup progress.
# Thread-safe: WarmupManager.status() returns a lock-guarded copy.
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
controller = _get_app_attr(app, "controller", None)
if controller and hasattr(controller, "warmup_status"):
try:
payload = controller.warmup_status()
except Exception:
payload = {"pending": [], "completed": [], "failed": []}
else:
payload = {"pending": [], "completed": [], "failed": []}
self.wfile.write(json.dumps(payload).encode("utf-8"))
elif self.path == "/api/warmup_wait" or self.path.startswith("/api/warmup_wait?"):
# Blocks the request thread (safe under ThreadingHTTPServer) up
# to `timeout` seconds waiting for warmup to complete, then
# returns the final status. Default timeout: 30s. Useful for
# external clients (scripts, other tools) that need to know when
# the system is fully ready before issuing AI requests.
timeout = 30.0
if "?" in self.path:
from urllib.parse import parse_qs, urlparse
qs = parse_qs(urlparse(self.path).query)
if "timeout" in qs:
try: timeout = float(qs["timeout"][0])
except (TypeError, ValueError): timeout = 30.0
controller = _get_app_attr(app, "controller", None)
if controller and hasattr(controller, "wait_for_warmup"):
try:
controller.wait_for_warmup(timeout=timeout)
except Exception: pass
try:
payload = controller.warmup_status()
except Exception:
payload = {"pending": [], "completed": [], "failed": []}
else:
payload = {"pending": [], "completed": [], "failed": []}
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(payload).encode("utf-8"))
else:
self.send_response(404)
self.end_headers()