From 62966fd7cf457982a0fc3b55198f93e50fab6194 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 3 Aug 2026 12:45:19 +0700 Subject: [PATCH] feat(dashboard): LLM Inference card with live llm-api metrics - /api/dashboard now queries llm-api Prometheus metrics: req/s, tok/s, gen tok/s, uptime, sparklines - new LLM Inference card shows gen throughput + uptime badge + traffic sparkline - fmtUptime helper (human-readable duration) --- src/app/api/dashboard/route.ts | 43 +++++++++++++++++++++ src/app/dashboard/page.tsx | 68 ++++++++++++++++++++++++++++++++++ src/lib/dashboard/types.ts | 19 ++++++++++ 3 files changed, 130 insertions(+) diff --git a/src/app/api/dashboard/route.ts b/src/app/api/dashboard/route.ts index d71b446..0dada7d 100644 --- a/src/app/api/dashboard/route.ts +++ b/src/app/api/dashboard/route.ts @@ -137,6 +137,14 @@ interface DashboardData { errors: number[]; traceVolume: number[]; links: { url: string; label: string }[]; + llm: { + reqRate: number | null; + tokRate: number | null; + tokSpeed: number | null; + uptime: number | null; + reqSpark: number[]; + tokSpark: number[]; + }; } export async function GET() { @@ -176,6 +184,33 @@ export async function GET() { promQuery("count(up)"), ]); + // ── LLM inference metrics (llm-api /metrics) ── + const [ + llmReqRate, + llmTokRate, + llmTokSpeed, + llmUptime, + llmReqSpark, + llmTokSpark, + ] = await Promise.all([ + promQuery( + `sum(rate(llm_api_requests_total{service="llm-api"}[5m]))`, + ), + promQuery( + `sum(rate(llm_api_completion_tokens_total{service="llm-api"}[5m]))`, + ), + promQuery( + `avg(llm_api_tokens_per_second{service="llm-api"})`, + ), + promQuery(`llm_api_uptime_seconds{service="llm-api"}`), + promRange( + `sum(rate(llm_api_requests_total{service="llm-api"}[1m]))`, + ), + promRange( + `sum(rate(llm_api_completion_tokens_total{service="llm-api"}[1m]))`, + ), + ]); + const links: { url: string; label: string }[] = [ { url: "https://github.com/asepharyana/asepharyana-hub", label: "GitHub" }, ]; @@ -195,6 +230,14 @@ export async function GET() { errors, traceVolume: traceVolume ? [traceVolume] : [], links, + llm: { + reqRate: llmReqRate, + tokRate: llmTokRate, + tokSpeed: llmTokSpeed, + uptime: llmUptime, + reqSpark: llmReqSpark, + tokSpark: llmTokSpark, + }, }; return NextResponse.json(data); diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 848e9a3..ad99820 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { AnimatedNumber } from "@/components/ui/motion-primitives"; import { type DashboardData, + fmtUptime, gaugeColor, safeDur, serviceIndicator, @@ -230,6 +231,73 @@ export default function Dashboard() { )} + {/* LLM Inference */} + + + + LLM Inference + + {data?.llm.uptime != null + ? `${fmtUptime(data.llm.uptime)} up` + : "no data"} + + + +
+ + + + +
+ {data?.llm.reqSpark?.length ? ( +
+ +
+ ) : ( +

+ No traffic yet — send a prompt on{" "} + + ai.asepharyana.my.id + +

+ )} +
+
+
+ {/* Request Rate */} {hasTraffik && ( diff --git a/src/lib/dashboard/types.ts b/src/lib/dashboard/types.ts index 6fc0fdc..c11af7a 100644 --- a/src/lib/dashboard/types.ts +++ b/src/lib/dashboard/types.ts @@ -30,6 +30,14 @@ export interface DashboardData { errors: number[]; traceVolume: number[]; links: { url: string; label: string }[]; + llm: { + reqRate: number | null; + tokRate: number | null; + tokSpeed: number | null; + uptime: number | null; + reqSpark: number[]; + tokSpark: number[]; + }; } export function safeDur(us: number): string { @@ -55,3 +63,14 @@ export function serviceIndicator(state: string): string { return "bg-red-400"; } } + +export function fmtUptime(seconds: number): string { + const s = Math.max(0, Math.floor(seconds)); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ${s % 60}s`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ${m % 60}m`; + const d = Math.floor(h / 24); + return `${d}d ${h % 24}h`; +}