Files
llm-monitor/tests/test_real_contracts.py
ducoterra 48bc92b9af
Build and Push Container / build-and-push (push) Successful in 38s
initial import: read-only gufo + vLLM dashboard
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
2026-10-16 12:49:45 -04:00

230 lines
9.9 KiB
Python

"""Ground truth: the payloads captured from the DEPLOYED engines.
Neither engine exports the series names people expect, and gufo's `/slots` is a bare
JSON array. These fixtures are verbatim captures (gufo on deskwork-ai:8731, vLLM on
driveripper-ai:8000); if a field stops mapping, the engine renamed something and this
test tells you which one instead of the dashboard quietly showing `n/a` in production.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
import pytest
from app import RateEstimator, parse_gufo_metrics, parse_vllm_metrics
FIXTURES = Path(__file__).resolve().parent / "fixtures"
@pytest.fixture(scope="module")
def gufo_text() -> str:
return (FIXTURES / "gufo_metrics_real.txt").read_text()
@pytest.fixture(scope="module")
def vllm_text() -> str:
return (FIXTURES / "vllm_metrics_real.txt").read_text()
# --------------------------------------------------------------------------- #
# gufo
# --------------------------------------------------------------------------- #
def test_gufo_real_names_are_the_primary_aliases(gufo_text: str) -> None:
m = parse_gufo_metrics(gufo_text)
assert m["tokens_prompt_total"]["source"] == "llamacpp:prompt_tokens_total"
assert m["tokens_generated_total"]["source"] == "llamacpp:tokens_predicted_total"
# gufo names its speed gauges *_seconds while the unit is tokens/second
assert m["prompt_tokens_per_second_native"]["source"] == "llamacpp:prompt_tokens_seconds"
assert m["decode_tokens_per_second_native"]["source"] == "llamacpp:predicted_tokens_seconds"
# parsed without an estimator, both throughput cells degrade to those one-request gauges
assert m["prompt_tokens_per_second"]["source"] == "llamacpp:prompt_tokens_seconds"
# parsed without an estimator, the throughput cell degrades to that one-request gauge
assert m["decode_tokens_per_second"]["source"] == "llamacpp:predicted_tokens_seconds"
def test_gufo_aggregate_decode_rate_resolves_on_the_real_counter(gufo_text: str) -> None:
"""The Δ-rates key off the RESOLVED counter names, so they must work on gufo's real
names (`tokens_predicted_total`, `prompt_tokens_total`) and land in the same ballpark as
gufo's own busy-time counter (`tokens_predicted_seconds_total`).
"""
rates = RateEstimator()
parse_gufo_metrics(gufo_text, rates, now=0.0)
bumped = gufo_text.replace(
"llamacpp:tokens_predicted_total 173091", "llamacpp:tokens_predicted_total 173191"
).replace(
# +1000 generated and +1000 prompt tokens over 2s -> 500 tok/s of each
"llamacpp:prompt_tokens_total 374263", "llamacpp:prompt_tokens_total 375263"
)
assert bumped != gufo_text # the capture moved the counter; re-check this test
m = parse_gufo_metrics(bumped, rates, now=2.0)
d = m["decode_tokens_per_second"]
assert d["source"] == "computed:Δllamacpp:tokens_predicted_total/Δt"
assert d["value"] == pytest.approx(50.0)
# sanity: the lifetime mean from the two cumulative counters is the same order of
# magnitude, so the windowed aggregate is not quietly summing something else
busy = float(re.search(r"tokens_predicted_seconds_total ([0-9.]+)", gufo_text).group(1))
assert 5.0 < (173091.0 / busy) < 100.0
# prompt throughput from the same two scrapes: gufo's prompt counter EXCLUDES cache hits,
# so this is newly-computed prefill work across all slots, not prompt tokens accepted
p = m["prompt_tokens_per_second"]
assert p["source"] == "computed:Δllamacpp:prompt_tokens_total/Δt"
assert p["value"] == pytest.approx(500.0)
assert m["prompt_tokens_per_second_native"]["value"] == pytest.approx(820.2841172907755)
def test_gufo_every_banner_and_strip_field_resolves(gufo_text: str) -> None:
m = parse_gufo_metrics(gufo_text)
for key in (
"requests_processing",
"requests_deferred",
"kv_cache_usage_ratio",
"tokens_prompt_total",
"tokens_generated_total",
"prompt_tokens_per_second",
"decode_tokens_per_second",
"tokens_total",
"n_tokens_max",
"device_lost_total",
):
assert m[key]["value"] is not None, f"gufo {key} stopped resolving"
assert 0.0 <= m["kv_cache_usage_ratio"]["value"] <= 1.0
def test_gufo_totals_include_cache_hits_which_are_a_separate_counter(gufo_text: str) -> None:
m = parse_gufo_metrics(gufo_text)
prompt = m["tokens_prompt_total"]["value"]
cached = m["tokens_prompt_cached_total"]["value"]
generated = m["tokens_generated_total"]["value"]
assert m["tokens_total"]["value"] == pytest.approx(prompt + cached + generated)
# 29.6M cached vs 0.37M computed on the live box — cache hits dominate
assert m["prompt_cache_hit_ratio"]["value"] == pytest.approx(cached / (prompt + cached))
assert 0.0 < m["prompt_cache_hit_ratio"]["value"] < 1.0
def test_gufo_speculative_decoding_accept_rate(gufo_text: str) -> None:
m = parse_gufo_metrics(gufo_text)
accept = m["spec_accept_rate"]
assert accept["derived"] is True
assert 0.0 <= accept["value"] <= 1.0
def test_gufo_device_lost_is_zero_on_a_healthy_box(gufo_text: str) -> None:
assert parse_gufo_metrics(gufo_text)["device_lost_total"]["value"] == 0.0
# --------------------------------------------------------------------------- #
# gufo /slots
# --------------------------------------------------------------------------- #
def test_gufo_slots_is_a_bare_array_not_an_object() -> None:
"""The UI must not assume {"slots": [...]} — the deployed engine returns a list."""
data = json.loads((FIXTURES / "gufo_slots_real.json").read_text())
assert isinstance(data, list)
assert len(data) >= 1
assert all(isinstance(slot, dict) and "id" in slot for slot in data)
def test_gufo_slot_fields_the_cards_depend_on() -> None:
data = json.loads((FIXTURES / "gufo_slots_real.json").read_text())
slot = data[0]
for key in (
"id",
"state",
"is_processing",
"id_task",
"n_prompt_tokens",
"n_prompt_tokens_cache",
"n_ctx",
"next_token",
"model",
):
assert key in slot, f"gufo slot no longer reports {key}"
assert slot["next_token"], "next_token[] should still be present, with n_decoded/n_remain"
assert "n_decoded" in slot["next_token"][0]
# an idle slot reports -1 for "nothing pending", which is not a count
if not slot["is_processing"]:
assert slot["id_task"] == -1
# --------------------------------------------------------------------------- #
# vLLM
# --------------------------------------------------------------------------- #
def test_vllm_real_names_are_the_primary_aliases(vllm_text: str) -> None:
m = parse_vllm_metrics(vllm_text)
assert m["gpu_cache_usage"]["source"] == "vllm:kv_cache_usage_perc" # not gpu_cache_usage
assert m["generation_tokens_total"]["source"] == "vllm:generation_tokens_total"
assert m["time_per_output_token_seconds"]["source"] == "vllm:inter_token_latency_seconds"
def test_vllm_every_banner_and_card_field_resolves(vllm_text: str) -> None:
m = parse_vllm_metrics(vllm_text)
for key in (
"num_requests_running",
"num_requests_waiting",
"gpu_cache_usage",
"prompt_tokens_total",
"generation_tokens_total",
"tokens_total",
"waiting_capacity",
"prefix_cache_hit_ratio",
"num_preemptions_total",
):
assert m[key]["value"] is not None, f"vLLM {key} stopped resolving"
for key in (
"time_to_first_token_seconds__mean",
"time_per_output_token_seconds__mean",
"request_queue_time_seconds__mean",
):
assert m[key]["value"] is not None, f"vLLM {key} stopped resolving"
assert 0.0 <= m["gpu_cache_usage"]["value"] <= 1.0
assert 0.0 <= m["prefix_cache_hit_ratio"]["value"] <= 1.0
def test_vllm_waiting_reasons_from_the_live_labels(vllm_text: str) -> None:
# the deployed build tags reasons as capacity/deferred, not defragmentation
m = parse_vllm_metrics(vllm_text)
assert "capacity" in m["waiting_by_reason"]["value"]
def test_vllm_histograms_survive_engine_and_model_labels(vllm_text: str) -> None:
m = parse_vllm_metrics(vllm_text)
ttft = m["time_to_first_token_seconds"]
assert ttft["count"] > 0 and ttft["mean"] > 0
# single engine label set here, so an interpolated p95 is legitimate
assert ttft["p95"] is None or ttft["p95"] >= ttft["mean"]
def test_vllm_prompt_tok_s_is_answered_even_without_a_rate_gauge(vllm_text: str) -> None:
# this build exports no throughput series at all, so prompt tok/s comes from the
# prefill histograms; it must not render n/a on a healthy, busy engine
m = parse_vllm_metrics(vllm_text)
p = m["prompt_tokens_per_second"]
assert p["value"] is not None, "prompt tok/s regressed to n/a"
assert p["derived"] is True
assert p["source"] == "computed:mean(request_prompt_tokens)/mean(request_prefill_time_seconds)"
assert p["value"] > 1000 # live box sits around 4.7k tok/s; single digits mean a bug
assert m["prefill_tokens_per_second_lifetime"]["value"] == pytest.approx(p["value"])
def test_vllm_prompt_tok_s_switches_to_measured_rate_once_scraping(vllm_text: str) -> None:
from app import RateEstimator
rates = RateEstimator()
parse_vllm_metrics(vllm_text, rates, now=1000.0)
m = parse_vllm_metrics(vllm_text, rates, now=1001.0) # same counters -> zero throughput
p = m["prompt_tokens_per_second"]
assert p["source"] == "computed:Δvllm:prompt_tokens_total/Δt"
assert p["window"] == pytest.approx(1.0)
# unchanged capture -> genuinely zero throughput in that window, not a missing gauge
assert p["value"] == 0.0
def test_vllm_decode_throughput_is_derived_on_this_build(vllm_text: str) -> None:
# no vllm:*throughput gauge exists on the deployed version -> computed from TPOT
m = parse_vllm_metrics(vllm_text)
assert m["decode_tokens_per_second"]["derived"] is True
assert m["decode_tokens_per_second"]["source"] == "computed:running/tpot"