Build and Push Container / build-and-push (push) Successful in 38s
Single-file FastAPI proxy plus a no-CDN vanilla-JS page for watching a gufo
server and a vLLM server side by side: one Prometheus parser (class Prom) with
per-engine field maps, measured Δcounter/Δt rates for engines that export no
rate gauge, and strict read-only GET-only routing. Engines are opt-in via
*_BASE_URL — unset means no panel, no poll, and a 404 {"configured": false}
without dialling.
Includes the container image and CI so the dashboard can be deployed as an
image rather than a checkout:
- Containerfile: uv-locked two-stage build on python:3.14-slim, non-root
(uid 10001), binds 8090 (8000 is vLLM's port in this fleet), healthcheck on
/api/config so a dead engine never marks the dashboard unhealthy. No frontend
build stage — static/index.html is bundler-free by design and is copied as-is.
- .gitea/workflows/build-and-push.yml: publishes
gitea.reeseapps.com/Experimental/llm-monitor on main + release tags.
114 tests, no network and no real engines required.
LLM-Generated: true
300 lines
13 KiB
Python
300 lines
13 KiB
Python
"""Proxy routes: read-only guarantee, upstream failure translation, JSON shape.
|
|
|
|
Nothing here touches a socket — see conftest.FakeUpstream. The point is to pin
|
|
the contract the browser depends on: what passes through, what becomes JSON, and
|
|
exactly how an engine outage shows up.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
import app as app_module
|
|
from tests.conftest import GUFO_FIXTURE, SLOTS_FIXTURE, VLLM_FIXTURE, response
|
|
|
|
API_PATHS = ["/api/gufo/slots", "/api/gufo/metrics", "/api/vllm/metrics", "/api/config"]
|
|
GUFO_PATHS = ["/api/gufo/slots", "/api/gufo/metrics"]
|
|
VLLM_PATHS = ["/api/vllm/metrics"]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# read-only by construction
|
|
# --------------------------------------------------------------------------- #
|
|
@pytest.mark.parametrize("method", ["post", "put", "patch", "delete"])
|
|
@pytest.mark.parametrize("path", API_PATHS)
|
|
def test_no_writing_verbs(client, method: str, path: str) -> None:
|
|
assert getattr(client, method)(path).status_code == 405
|
|
|
|
|
|
def test_only_get_reaches_upstream(client, upstream) -> None:
|
|
upstream.route("http://gufo.test/slots", response(200, SLOTS_FIXTURE.encode()))
|
|
client.get("/api/gufo/slots")
|
|
upstream.route("http://gufo.test/metrics", response(200, GUFO_FIXTURE.encode()))
|
|
client.get("/api/gufo/metrics")
|
|
assert upstream.methods == {"GET"}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# /api/gufo/slots — passthrough
|
|
# --------------------------------------------------------------------------- #
|
|
def test_slots_pass_through_byte_for_byte(client, upstream) -> None:
|
|
upstream.route(
|
|
"http://gufo.test/slots", response(200, SLOTS_FIXTURE.encode(), "application/json")
|
|
)
|
|
r = client.get("/api/gufo/slots")
|
|
assert r.status_code == 200
|
|
assert r.content.decode() == SLOTS_FIXTURE # not re-serialised, not reformatted
|
|
assert r.headers["content-type"].startswith("application/json")
|
|
assert "no-store" in r.headers["cache-control"]
|
|
assert r.json()["slots"][0]["id_task"] == 12
|
|
|
|
|
|
def test_slots_accepts_a_bare_array_payload(client, upstream) -> None:
|
|
"""The deployed gufo returns `[{...}]`, not `{"slots": [...]}`. The proxy must not
|
|
care — it forwards whatever JSON the engine sent, unchanged."""
|
|
bare = '[{"id":0,"is_processing":true,"id_task":7,"next_token":[{"n_decoded":3,"n_remain":9}]}]'
|
|
upstream.route("http://gufo.test/slots", response(200, bare.encode(), "application/json"))
|
|
r = client.get("/api/gufo/slots")
|
|
assert r.status_code == 200
|
|
assert r.json() == json.loads(bare)
|
|
assert isinstance(r.json(), list)
|
|
|
|
|
|
def test_upstream_content_type_is_passed_through(client, upstream) -> None:
|
|
# whatever gufo claims, the browser gets the same thing — no re-typing
|
|
upstream.route(
|
|
"http://gufo.test/slots",
|
|
response(200, SLOTS_FIXTURE.encode(), "application/stream+json"),
|
|
)
|
|
assert client.get("/api/gufo/slots").headers["content-type"] == "application/stream+json"
|
|
|
|
|
|
def test_slots_falls_back_to_the_alias(client, upstream) -> None:
|
|
# /slots 404s (the stub's default for unrouted URLs), the alias answers
|
|
upstream.route("http://gufo.test/v1/slots", response(200, SLOTS_FIXTURE.encode(), "application/json"))
|
|
r = client.get("/api/gufo/slots")
|
|
assert r.status_code == 200
|
|
assert upstream.requested_urls == ["http://gufo.test/slots", "http://gufo.test/v1/slots"]
|
|
|
|
|
|
def test_slots_reports_missing_endpoints(client, upstream) -> None:
|
|
r = client.get("/api/gufo/slots")
|
|
assert r.status_code == 502
|
|
assert "/slots" in r.json()["detail"] and "/v1/slots" in r.json()["detail"]
|
|
|
|
|
|
def test_slots_does_not_swallow_a_server_error(client, upstream) -> None:
|
|
upstream.route("http://gufo.test/slots", response(500, b"boom"))
|
|
r = client.get("/api/gufo/slots")
|
|
assert r.status_code == 502 # engine is unhealthy: that is a bad gateway, not a fallback
|
|
assert "HTTP 500" in r.json()["detail"]
|
|
assert upstream.requested_urls == ["http://gufo.test/slots"] # no pointless alias retry
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# /metrics — Prometheus -> JSON
|
|
# --------------------------------------------------------------------------- #
|
|
def test_gufo_metrics_endpoint(client, upstream) -> None:
|
|
upstream.route("http://gufo.test/metrics", response(200, GUFO_FIXTURE.encode()))
|
|
body = client.get("/api/gufo/metrics").json()
|
|
assert body["ok"] is True
|
|
assert body["engine"] == "gufo"
|
|
assert body["upstream"] == "http://gufo.test/metrics"
|
|
assert body["series"] == 8 # 7 llamacpp:* series + the legacy llama:* one
|
|
assert body["unparsed"] == 0
|
|
assert body["metrics"]["requests_processing"]["value"] == 2.0
|
|
assert body["scraped_at"] > 0
|
|
|
|
|
|
def test_vllm_metrics_endpoint(client, upstream) -> None:
|
|
upstream.route("http://vllm.test/metrics", response(200, VLLM_FIXTURE.encode()))
|
|
m = client.get("/api/vllm/metrics").json()["metrics"]
|
|
assert m["num_requests_running"]["value"] == 3.0
|
|
assert m["waiting_capacity"]["value"] == 5.0
|
|
assert m["time_to_first_token_seconds"]["mean"] == pytest.approx(1.0)
|
|
|
|
|
|
def test_garbage_exposition_is_ok_with_no_series(client, upstream) -> None:
|
|
upstream.route("http://vllm.test/metrics", response(200, b"not prometheus at all\n"))
|
|
body = client.get("/api/vllm/metrics").json()
|
|
assert body["ok"] is True
|
|
assert body["series"] == 0 and body["unparsed"] == 1
|
|
assert body["metrics"]["num_requests_running"]["value"] is None # renders as n/a
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# engine outage translation (the per-engine health contract)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_unreachable_engine_is_a_502_with_a_readable_detail(client, upstream) -> None:
|
|
upstream.route("http://vllm.test/metrics", httpx.ConnectError("connection refused"))
|
|
r = client.get("/api/vllm/metrics")
|
|
assert r.status_code == 502
|
|
assert r.json()["ok"] is False
|
|
assert "unreachable" in r.json()["detail"]
|
|
|
|
|
|
def test_slow_engine_is_a_504(client, upstream) -> None:
|
|
upstream.route("http://gufo.test/metrics", httpx.TimeoutException("too slow"))
|
|
r = client.get("/api/gufo/metrics")
|
|
assert r.status_code == 504
|
|
assert "timed out" in r.json()["detail"]
|
|
|
|
|
|
def test_one_engine_being_down_does_not_affect_the_other(client, upstream) -> None:
|
|
upstream.route("http://gufo.test/slots", response(200, SLOTS_FIXTURE.encode(), "application/json"))
|
|
upstream.route("http://vllm.test/metrics", httpx.ConnectError("connection refused"))
|
|
assert client.get("/api/vllm/metrics").status_code == 502
|
|
assert client.get("/api/gufo/slots").status_code == 200
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# auth + config
|
|
# --------------------------------------------------------------------------- #
|
|
def test_no_authorization_header_without_keys(client, upstream) -> None:
|
|
upstream.route("http://gufo.test/slots", response(200, b"{}", "application/json"))
|
|
upstream.route("http://vllm.test/metrics", response(200, b"", "text/plain"))
|
|
client.get("/api/gufo/slots")
|
|
client.get("/api/vllm/metrics")
|
|
assert all("authorization" not in headers for _url, headers in upstream.calls)
|
|
|
|
|
|
def test_bearer_tokens_are_per_engine(client, upstream, monkeypatch) -> None:
|
|
monkeypatch.setattr(app_module, "GUFO_API_KEY", "sk-gufo")
|
|
monkeypatch.setattr(app_module, "VLLM_API_KEY", "sk-vllm")
|
|
upstream.route("http://gufo.test/slots", response(200, b"{}", "application/json"))
|
|
upstream.route("http://vllm.test/metrics", response(200, b"", "text/plain"))
|
|
client.get("/api/gufo/slots")
|
|
client.get("/api/vllm/metrics")
|
|
assert upstream.calls[0][1]["Authorization"] == "Bearer sk-gufo"
|
|
assert upstream.calls[1][1]["Authorization"] == "Bearer sk-vllm"
|
|
|
|
|
|
def test_config_never_leaks_keys(client, monkeypatch) -> None:
|
|
monkeypatch.setattr(app_module, "GUFO_API_KEY", "sk-super-secret")
|
|
r = client.get("/api/config")
|
|
assert "sk-super-secret" not in r.text
|
|
body = r.json()
|
|
assert body["gufo_auth"] is True and body["vllm_auth"] is False
|
|
assert body["poll_ms"] == app_module.POLL_MS
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# opt-in engines: an unset *_BASE_URL means this deployment does not run that server
|
|
# --------------------------------------------------------------------------- #
|
|
@pytest.mark.parametrize(
|
|
"raw, expected",
|
|
[
|
|
("http://box:1", "http://box:1"),
|
|
("http://box:1/", "http://box:1"), # never a doubled //metrics
|
|
(" http://box:1 ", "http://box:1"),
|
|
("", ""), # set-but-empty is 'not configured', not 'http:///metrics'
|
|
(" ", ""),
|
|
],
|
|
)
|
|
def test_base_url_env_parsing(monkeypatch, raw: str, expected: str) -> None:
|
|
monkeypatch.setenv("TEST_ENGINE_BASE_URL", raw)
|
|
assert app_module._base_url_from_env("TEST_ENGINE_BASE_URL") == expected
|
|
# and unset means the same as blank: no default host is invented
|
|
monkeypatch.delenv("TEST_ENGINE_BASE_URL")
|
|
assert app_module._base_url_from_env("TEST_ENGINE_BASE_URL") == ""
|
|
|
|
|
|
@pytest.mark.parametrize("path", GUFO_PATHS + VLLM_PATHS)
|
|
def test_unset_base_url_is_404_not_configured(client, upstream, monkeypatch, path: str) -> None:
|
|
"""A server nobody runs must not look like a server that is down: no upstream call,
|
|
no 502, and a detail that names the env var to set."""
|
|
engine = "gufo" if "gufo" in path else "vllm"
|
|
monkeypatch.setattr(app_module, f"{engine.upper()}_BASE_URL", "")
|
|
r = client.get(path)
|
|
assert r.status_code == 404
|
|
body = r.json()
|
|
assert body["ok"] is False
|
|
assert body["configured"] is False
|
|
assert body["engine"] == engine
|
|
assert f"{engine.upper()}_BASE_URL" in body["detail"]
|
|
assert "no such route" not in body["detail"] # distinct from an unknown path
|
|
assert upstream.requested_urls == [] # nothing was dialled
|
|
|
|
|
|
def test_one_configured_engine_is_unaffected_by_the_other_being_absent(
|
|
client, upstream, monkeypatch
|
|
) -> None:
|
|
"""The vLLM-only recipient of this repo gets a working dashboard, not a half-broken one."""
|
|
monkeypatch.setattr(app_module, "GUFO_BASE_URL", "")
|
|
upstream.route("http://vllm.test/metrics", response(200, VLLM_FIXTURE.encode()))
|
|
assert client.get("/api/gufo/metrics").status_code == 404
|
|
assert client.get("/api/gufo/slots").status_code == 404
|
|
body = client.get("/api/vllm/metrics").json()
|
|
assert body["ok"] is True
|
|
assert upstream.requested_urls == ["http://vllm.test/metrics"]
|
|
|
|
|
|
def test_config_reports_which_engines_exist(client, monkeypatch) -> None:
|
|
body = client.get("/api/config").json()
|
|
assert body["gufo_enabled"] is True and body["vllm_enabled"] is True
|
|
|
|
monkeypatch.setattr(app_module, "GUFO_BASE_URL", "")
|
|
body = client.get("/api/config").json()
|
|
assert body["gufo_enabled"] is False
|
|
assert body["gufo_base_url"] == ""
|
|
assert body["vllm_enabled"] is True # the other engine keeps its section
|
|
|
|
|
|
def test_lifespan_owns_one_shared_client(client) -> None:
|
|
"""Entering lifespan installs exactly one AsyncClient on app.state and closes it
|
|
on shutdown — the poll loop depends on connection reuse, and a leaked client
|
|
would keep sockets open after Ctrl-C."""
|
|
from fastapi.testclient import TestClient
|
|
|
|
with TestClient(app_module.app) as running:
|
|
shared = app_module.app.state.client
|
|
assert isinstance(shared, httpx.AsyncClient)
|
|
assert not shared.is_closed
|
|
assert running.get("/api/config").status_code == 200
|
|
assert app_module.app.state.client is shared
|
|
assert shared.is_closed
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# page + misc
|
|
# --------------------------------------------------------------------------- #
|
|
def test_dashboard_is_served(client) -> None:
|
|
r = client.get("/")
|
|
assert r.status_code == 200
|
|
assert "text/html" in r.headers["content-type"]
|
|
assert "LLM Monitor" in r.text
|
|
|
|
|
|
def test_dashboard_layout_is_driven_by_the_config_flags(client) -> None:
|
|
"""The page must render from /api/config's *_enabled flags instead of hardcoding two
|
|
engines: an absent engine loses its panel, its strip card, its polling and its share
|
|
of the fleet totals, and nothing configured at all shows the setup card. Asserted on
|
|
the shipped HTML because the layout logic lives in the inline script."""
|
|
html = client.get("/").text
|
|
# one generic engine list, revealed from the backend's flags
|
|
assert "const ENGINE_IDS" in html and "_enabled`" in html
|
|
assert "isEnabled(job.engine)" in html # an absent engine is never polled
|
|
# what gets hidden, and what shows instead when there is nothing to show
|
|
for marker in ("panel-${engine}", "strip-${engine}", 'id="no-engines"'):
|
|
assert marker in html
|
|
|
|
|
|
def test_dashboard_loads_no_third_party_assets(client) -> None:
|
|
"""The engines live on an internet-disconnected network: no CDN, no webfonts,
|
|
no build step. Only same-origin requests are allowed from the page."""
|
|
import re
|
|
|
|
html = client.get("/").text
|
|
refs = re.findall(r"""(?:src|href)\s*=\s*["']([^"']+)["']""", html)
|
|
external = [r for r in refs if r.startswith(("http://", "//"))]
|
|
assert external == [], f"page references external assets: {external}"
|
|
|
|
|
|
def test_unknown_route_is_json(client) -> None:
|
|
r = client.get("/api/nope")
|
|
assert r.status_code == 404
|
|
assert "no such route" in r.json()["detail"]
|