Initial commit: router front-door vLLM stack

Three always-running vLLM services (text TP=2 GPU0+1, ocr + embed on GPU2,
sleep mode) behind a FastAPI router that auto-wakes models on request.
Tiered idle (sleep 15 min / offload 3 h), depth-aware 503s with
Retry-After, persisted wake-intent recovery, admin API on 127.0.0.1:8010.
Routine control via vllmctl is pure HTTP — no docker on the request path.

Verified: 91 router unit tests + 15-test E2E on real hardware
(measurements in CALIBRATION.md; design record in
.claude/memory/router-front-door-plan.md).

Old nginx stack files removed before git init; design survives in
.claude/memory/sleep-mode-implementation-plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 10:17:42 +00:00
commit 80eef4ce6a
35 changed files with 6506 additions and 0 deletions

143
router/admin_api.py Normal file
View File

@@ -0,0 +1,143 @@
"""Admin listener (the :8010 socket) -- `vllmctl` / debugging surface.
This app is mounted on a *separate socket*, never on the public one (plan
review C5: a public /admin/sleep would be a trivial remote DoS). It shares
the exact same ServiceManager, locks and depth tracking as the request path.
The socket binds 0.0.0.0 *inside the container*; the host-side "localhost
only" restriction comes from compose publishing `127.0.0.1:8010:8010`.
"""
from __future__ import annotations
import logging
import time
from typing import Any
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from config import DEPTH_AWAKE
from services import ServiceManager
log = logging.getLogger("vllm_router.admin")
def build_admin_app(manager: ServiceManager) -> FastAPI:
cfg = manager.cfg
app = FastAPI(title="vllm-router admin", version="1.0.0",
docs_url=None, redoc_url=None, openapi_url=None)
def service_or_404(key: str) -> tuple[Any, JSONResponse | None]:
# Accepts the service key ("text") or the model name / alias
# ("OvisOCR2", "ocr", "qwen3.6-35b-a3b-fp8"), case-insensitively.
resolved = key.strip().lower()
if resolved not in manager.services:
resolved = cfg.index.get(key.strip().lower(), "")
svc = manager.services.get(resolved)
if svc is None:
body = {"error": {"message": f"Unknown service or model '{key}'.",
"type": "invalid_request_error", "code": "not_found",
"known": sorted(manager.services)}}
return None, JSONResponse(body, status_code=404)
return svc, None
@app.get("/admin/status")
async def admin_status() -> JSONResponse:
status = await manager.status(live_probe=True)
return JSONResponse({
"router": {
"uptime_s": status["uptime_s"],
"public_port": cfg.public_port,
"admin_port": cfg.admin_port,
"idle": {
"enabled": cfg.idle_enabled,
"sleep_min": cfg.idle_sleep_min,
"offload_min": cfg.idle_offload_min,
},
},
"services": status["services"],
"api": "http://127.0.0.1:%d/v1" % cfg.public_port,
})
@app.post("/admin/wake/{key}")
async def admin_wake(key: str) -> JSONResponse:
svc, err = service_or_404(key)
if err is not None:
return err
outcome = await manager.ensure_awake(svc.cfg.key)
svc.last_activity = time.monotonic() # idle clock restarts
body = {
"service": svc.cfg.key,
"model": svc.cfg.model,
"ok": outcome.ok,
"depth": outcome.depth,
"reason": outcome.reason,
"wake_in_progress": svc.wake_in_progress,
}
if outcome.latency_s is not None:
body["latency_s"] = round(outcome.latency_s, 2)
if not outcome.ok:
policy = cfg.policy(outcome.depth)
body["retry_after_s"] = policy.retry_after_s
body["estimated_wake_seconds"] = policy.est_wake_s
body["error"] = {"type": "model_waking", "code": "model_waking",
"sleep_depth": outcome.depth,
"estimated_wake_seconds": policy.est_wake_s}
if outcome.detail:
body["error"]["message"] = outcome.detail
log.info("admin_wake service=%s ok=false depth=%s reason=%s",
svc.cfg.key, outcome.depth, outcome.reason)
return JSONResponse(body, status_code=503,
headers={"Retry-After": str(policy.retry_after_s)})
log.info("admin_wake service=%s ok=true depth=%s reason=%s",
svc.cfg.key, outcome.depth, outcome.reason)
return JSONResponse(body)
@app.post("/admin/sleep/{key}")
async def admin_sleep(key: str, request: Request) -> JSONResponse:
svc, err = service_or_404(key)
if err is not None:
return err
level = 1
if request.query_params.get("level"):
try:
level = int(request.query_params["level"])
except ValueError:
return JSONResponse(
{"error": {"message": "level must be 1 or 2",
"type": "invalid_request_error"}}, status_code=400)
if level not in (1, 2):
return JSONResponse(
{"error": {"message": "level must be 1 or 2",
"type": "invalid_request_error"}}, status_code=400)
result = await manager.sleep_service(svc.cfg.key, level, reason="admin")
status_code = 200 if result.get("ok") else 409
result["level_requested"] = level
result["model"] = svc.cfg.model
log.info("admin_sleep service=%s level=%d ok=%s reason=%s",
svc.cfg.key, level, result.get("ok"), result.get("reason"))
return JSONResponse(result, status_code=status_code)
@app.get("/health")
@app.get("/admin/health", include_in_schema=False)
async def admin_health() -> JSONResponse:
return JSONResponse({"status": "ok",
"awake": [k for k, s in manager.services.items()
if s.depth == DEPTH_AWAKE],
"wake_recovery_pending": [k for k, s in manager.services.items()
if s.pending_reload]})
@app.exception_handler(Exception)
async def internal_error(_request: Request, exc: Exception) -> JSONResponse:
if manager.shutting_down:
return JSONResponse(
{"error": {"type": "router_shutting_down", "code": "router_shutting_down",
"message": "Router is shutting down; retry shortly"}},
status_code=503, headers={"Retry-After": "5"})
return JSONResponse(
{"error": {"type": "internal_error", "code": "internal_error",
"message": f"Unhandled router error: {type(exc).__name__}"}},
status_code=500)
return app