Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24742c75b7 | ||
|
|
961713e92d | ||
|
|
01db29e754 | ||
|
|
ab06e4c221 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"uid": "llm-fleet",
|
||||
"title": "LLM fleet \u2014 capacity and reuse",
|
||||
"description": "The signals that explain the LLM endpoints under load. One metric namespace: llamacpp:* across all three endpoints — frank-zealot (Dankclaw, RTX 3090), frank-dragoon (Dankclaw, gfx1151 iGPU) and frank-archon (Dankserver, 2x R9700, llama.cpp again since the 2026-09-26 swap back from Lucebox DS4.1). The endpoint picker selects between them. See serverAppConfigs-docs/Infrastructure/Observability.md for the namespace split and the addressability rules.",
|
||||
"description": "The signals that explain the LLM endpoints under load, in TWO namespaces. Panels 1-6 are llama.cpp (llamacpp:*) and the endpoint picker ($job) selects frank-zealot (Dankclaw, RTX 3090) and frank-dragoon (Dankclaw, gfx1151 iGPU). frank-archon (Dankserver, 2x R9700) is radiance, NOT llama.cpp: it emits vllm:* and radiance:* (job=frank-archon) and has its own section at the bottom - those families are alert-on but were never graphed here until now. See serverAppConfigs-docs/Infrastructure/Observability.md.",
|
||||
"tags": [
|
||||
"llm",
|
||||
"capacity"
|
||||
@@ -212,6 +212,98 @@
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"type": "row",
|
||||
"title": "frank-archon \u2014 radiance (vllm:* + radiance:*)",
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 33 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 101, "type": "timeseries", "title": "Archon throughput (tokens/s)",
|
||||
"description": "vLLM-compat counters: prompt and generation token rates for frank-archon.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 34 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "short", "min": 0, "custom": { "fillOpacity": 10, "lineWidth": 1 } }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "sum(rate(vllm:prompt_tokens_total{job=\"frank-archon\"}[5m]))", "legendFormat": "prompt/s", "refId": "A" },
|
||||
{ "expr": "sum(rate(vllm:generation_tokens_total{job=\"frank-archon\"}[5m]))", "legendFormat": "generation/s", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 102, "type": "timeseries", "title": "Archon request outcomes",
|
||||
"description": "Completed requests/s by finish reason (stop / length / tool_calls / abort).",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 34 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{ "expr": "sum by (finished_reason)(rate(vllm:request_success_total{job=\"frank-archon\"}[5m]))", "legendFormat": "{{finished_reason}}", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 103, "type": "timeseries", "title": "Archon queue and preemptions",
|
||||
"description": "num_requests_waiting is the queue gauge (VllmRequestsQueueing fires on >0); preemption rate (VllmPreempting).",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 42 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{ "expr": "vllm:num_requests_waiting{job=\"frank-archon\"}", "legendFormat": "waiting", "refId": "A" },
|
||||
{ "expr": "sum(rate(vllm:num_preemptions_total{job=\"frank-archon\"}[5m]))", "legendFormat": "preemptions/s", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 104, "type": "timeseries", "title": "Archon time-to-first-token",
|
||||
"description": "TTFT p50/p95 from the vLLM histogram. VllmTtftSlow alerts when the p95 recording rule exceeds 120 s.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 42 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "s", "min": 0, "custom": { "fillOpacity": 10, "lineWidth": 1 } }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "histogram_quantile(0.5, sum by (le)(rate(vllm:time_to_first_token_seconds_bucket{job=\"frank-archon\"}[5m])))", "legendFormat": "p50", "refId": "A" },
|
||||
{ "expr": "histogram_quantile(0.95, sum by (le)(rate(vllm:time_to_first_token_seconds_bucket{job=\"frank-archon\"}[5m])))", "legendFormat": "p95", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 105, "type": "timeseries", "title": "Archon radiance decode/prefill t/s",
|
||||
"description": "radiance-native instantaneous throughput gauges - the engine's own view, not the vLLM counter derivative.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 50 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{ "expr": "radiance:decode_tokens_per_second{job=\"frank-archon\"}", "legendFormat": "decode t/s", "refId": "A" },
|
||||
{ "expr": "radiance:prefill_tokens_per_second{job=\"frank-archon\"}", "legendFormat": "prefill t/s", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 106, "type": "timeseries", "title": "Archon speculative decode (MTP)",
|
||||
"description": "radiance draft acceptance - the native signal vLLM metrics omit. The whole reason radiance exposes its own endpoint.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 50 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "percentunit", "min": 0, "max": 1, "custom": { "fillOpacity": 10, "lineWidth": 1 } }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "radiance:draft_acceptance_ratio{job=\"frank-archon\"}", "legendFormat": "acceptance ratio", "refId": "A" },
|
||||
{ "expr": "sum(rate(radiance:draft_accepted_total{job=\"frank-archon\"}[5m])) / clamp_min(sum(rate(radiance:draft_tokens_total{job=\"frank-archon\"}[5m])), 0.001)", "legendFormat": "accepted/token", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 107, "type": "timeseries", "title": "Archon engine step time",
|
||||
"description": "radiance engine-step accounting: mean decode step duration (seconds per step) plus prefill steps/s.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 58 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "s", "min": 0, "custom": { "fillOpacity": 10, "lineWidth": 1 } }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "rate(radiance:engine_step_seconds_decode_total{job=\"frank-archon\"}[5m]) / clamp_min(rate(radiance:engine_steps_decode_total{job=\"frank-archon\"}[5m]), 0.001)", "legendFormat": "avg decode step", "refId": "A" },
|
||||
{ "expr": "sum(rate(radiance:engine_steps_prefill_total{job=\"frank-archon\"}[5m]))", "legendFormat": "prefill steps/s", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 108, "type": "timeseries", "title": "Archon prefix cache hit rate",
|
||||
"description": "vLLM and radiance prefix-cache hit rates. A collapse fires VllmPrefixCacheCollapse.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 58 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "percentunit", "min": 0, "max": 1, "custom": { "fillOpacity": 10, "lineWidth": 1 } }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "vllm:gpu_prefix_cache_hit_rate{job=\"frank-archon\"}", "legendFormat": "vllm prefix hit", "refId": "A" },
|
||||
{ "expr": "radiance:linear_prefix_cache_hit_rate{job=\"frank-archon\"}", "legendFormat": "radiance prefix hit", "refId": "B" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -31,8 +31,12 @@ export default defineConfig({
|
||||
// container name, so it needs its own pass condition). 18- joined on
|
||||
// 2026-09-24 (OpenWebUI SSO through authentik). 20- joined on 2026-10-21
|
||||
// (Grafana's OAuth2 relying-party login, which the edge-only 13- never
|
||||
// follows).
|
||||
testMatch: /(10|12|13|16|17|18|20)-.*\.spec\.ts/,
|
||||
// follows). 21- joined 2026-10-21 (the open-notebook vhost, deployed
|
||||
// 2026-10-19 - the newest guarded app, in neither GUARDED list nor any
|
||||
// spec until now: the 17-hermes "uncovered vhost broke silently" class).
|
||||
// 22- joins Prometheus's app-is-really-there proof (its own API), closing
|
||||
// the "not a login wall but maybe a 404" hole 20- closed for Grafana.
|
||||
testMatch: /(10|12|13|16|17|18|20|21|22)-.*\.spec\.ts/,
|
||||
use: { storageState: ".auth/qa-e2e.json" },
|
||||
dependencies: ["setup"],
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ const BASE = "https://www.franksdank.cloud";
|
||||
// Every vhost that Caddy routes through anubis:8923 (see dankclaw/Caddyfile).
|
||||
// Keep this list in lockstep with the Caddyfile: a new guarded vhost must be
|
||||
// added here or the guard-chain audit is incomplete.
|
||||
const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki"];
|
||||
const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki", "notebook"];
|
||||
|
||||
// Markers that prove app content leaked past the guard to an anonymous user.
|
||||
const LEAK_MARKERS: Record<string, string[]> = {
|
||||
@@ -25,6 +25,7 @@ const LEAK_MARKERS: Record<string, string[]> = {
|
||||
"seerr.franksdank.cloud": ["Request Movie"],
|
||||
"grafana.franksdank.cloud": ["Dashboards"],
|
||||
"prometheus.franksdank.cloud": ["Expression Browser", "Graph"],
|
||||
"notebook.franksdank.cloud": ["Open Notebook"],
|
||||
};
|
||||
|
||||
test("homepage root (/) is guarded for unauthenticated users", async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// open-notebook (notebook.franksdank.cloud), deployed 2026-10-19, is a guarded
|
||||
// vhost whose outpost (notebook-proxy) fronts an app with its OWN password
|
||||
// login - the same shape as hermes (17-) and OpenWebUI (18-): the guard chain
|
||||
// (Anubis PoW -> authentik outpost) must hand off to the app, and the app's own
|
||||
// login page is the expected landing. Until now this host appeared in neither
|
||||
// the 13-/14- GUARDED lists nor any dedicated spec, so a broken upstream would
|
||||
// have stayed invisible - the exact 17-hermes "uncovered vhost" failure class.
|
||||
// 14- now covers the unauthenticated gate; this spec owns authenticated
|
||||
// reachability with a POSITIVE app marker, not just "not a login wall".
|
||||
|
||||
const VHOST = "https://notebook.franksdank.cloud/";
|
||||
const CHALLENGE = "Making sure you're not a bot";
|
||||
|
||||
test("notebook vhost reaches the open-notebook app through the guard chain", async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
await page.goto(VHOST);
|
||||
|
||||
// The Anubis proof-of-work clears in a real browser and the outpost hands off
|
||||
// to the app; the user must never be parked on an authentik flow.
|
||||
await expect
|
||||
.poll(async () => !(await page.locator("body").innerText()).includes(CHALLENGE), { timeout: 90_000 })
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).hostname, { timeout: 60_000 })
|
||||
.toBe("notebook.franksdank.cloud");
|
||||
expect(
|
||||
new URL(page.url()).pathname.startsWith("/if/"),
|
||||
"must not be parked on an authentik flow",
|
||||
).toBe(false);
|
||||
expect(
|
||||
await page.locator('input[name="uidField"]').count(),
|
||||
"must not show the authentik login form",
|
||||
).toBe(0);
|
||||
|
||||
// Positive identity: the page really is open-notebook's own login, not a
|
||||
// Caddy error or the IdP shell. The app answering its own login is the pass.
|
||||
await expect(page, "destination title must be Open Notebook").toHaveTitle(/open notebook/i, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.locator("body"), "open-notebook login marker").toContainText(
|
||||
/enter your password/i,
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const body = await page.locator("body").innerText();
|
||||
expect(body, "must not leak the IdP flow shell").not.toContain("Powered by authentik");
|
||||
expect(body, "must not be a Caddy upstream error").not.toContain("Bad Gateway");
|
||||
|
||||
const resp = await page.request.get(VHOST);
|
||||
expect(resp.status(), "open-notebook must answer, not error").toBeLessThan(500);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// Prometheus (prometheus.franksdank.cloud) sits behind the same embedded outpost
|
||||
// as Grafana. 13-sso and 11-admin only ever assert it is "not a login wall and
|
||||
// under 500" - which a Prometheus serving a 404 or a blank page would ALSO pass.
|
||||
// This is the app-is-really-there proof 20- provides for Grafana: with the
|
||||
// standard session the outpost hands off silently to the Prometheus UI, and the
|
||||
// app's own API answers success. A guard-route or upstream regression that lets
|
||||
// any non-Prometheus answer through fails here.
|
||||
|
||||
test("SSO: Prometheus app answers its own API through authentik", async ({ page, request }) => {
|
||||
test.setTimeout(180_000);
|
||||
await page.goto("https://prometheus.franksdank.cloud/");
|
||||
|
||||
// Challenge clears, outpost hands off silently to the app (no credential prompt).
|
||||
await expect
|
||||
.poll(
|
||||
async () => !(await page.locator("body").innerText()).includes("Making sure you're not a bot"),
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).hostname, { timeout: 60_000 })
|
||||
.toBe("prometheus.franksdank.cloud");
|
||||
expect(await page.locator('input[name="uidField"]').count(), "no authentik form").toBe(0);
|
||||
await expect(page, "must land on the Prometheus UI").toHaveTitle(/prometheus/i, { timeout: 30_000 });
|
||||
|
||||
// Load-bearing assertion: the Prometheus API itself answers, authenticated,
|
||||
// through the outpost. An IdP bounce, a 404, or a raw upstream error fails here.
|
||||
const api = await request.get("https://prometheus.franksdank.cloud/api/v1/status/buildinfo");
|
||||
expect(api.status(), "Prometheus API must answer 200 through the guard").toBe(200);
|
||||
expect((await api.json()).status, "Prometheus API must report success").toBe("success");
|
||||
});
|
||||
Reference in New Issue
Block a user