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)
This commit is contained in:
asepharyana
2026-08-03 12:45:19 +07:00
parent 6829c7efe2
commit 62966fd7cf
3 changed files with 130 additions and 0 deletions
+43
View File
@@ -137,6 +137,14 @@ interface DashboardData {
errors: number[]; errors: number[];
traceVolume: number[]; traceVolume: number[];
links: { url: string; label: string }[]; 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() { export async function GET() {
@@ -176,6 +184,33 @@ export async function GET() {
promQuery("count(up)"), 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 }[] = [ const links: { url: string; label: string }[] = [
{ url: "https://github.com/asepharyana/asepharyana-hub", label: "GitHub" }, { url: "https://github.com/asepharyana/asepharyana-hub", label: "GitHub" },
]; ];
@@ -195,6 +230,14 @@ export async function GET() {
errors, errors,
traceVolume: traceVolume ? [traceVolume] : [], traceVolume: traceVolume ? [traceVolume] : [],
links, links,
llm: {
reqRate: llmReqRate,
tokRate: llmTokRate,
tokSpeed: llmTokSpeed,
uptime: llmUptime,
reqSpark: llmReqSpark,
tokSpark: llmTokSpark,
},
}; };
return NextResponse.json(data); return NextResponse.json(data);
+68
View File
@@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AnimatedNumber } from "@/components/ui/motion-primitives"; import { AnimatedNumber } from "@/components/ui/motion-primitives";
import { import {
type DashboardData, type DashboardData,
fmtUptime,
gaugeColor, gaugeColor,
safeDur, safeDur,
serviceIndicator, serviceIndicator,
@@ -230,6 +231,73 @@ export default function Dashboard() {
</motion.div> </motion.div>
)} )}
{/* LLM Inference */}
<motion.div variants={cardVariants}>
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>LLM Inference</CardTitle>
<Badge variant="secondary" className="font-mono text-[10px] font-normal">
{data?.llm.uptime != null
? `${fmtUptime(data.llm.uptime)} up`
: "no data"}
</Badge>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-1.5">
<StatBox
value={
data?.llm.reqRate != null
? data.llm.reqRate.toFixed(1)
: undefined
}
label="Req/s (5m)"
color="text-blue-400"
/>
<StatBox
value={
data?.llm.tokRate != null
? `${data.llm.tokRate.toFixed(0)}`
: undefined
}
label="Tokens/s (5m)"
color="text-green-400"
/>
<StatBox
value={
data?.llm.tokSpeed != null
? `${data.llm.tokSpeed.toFixed(0)}`
: undefined
}
label="Gen tok/s"
color="text-purple-400"
/>
<StatBox
value={data?.llm.reqSpark?.length ? "live" : undefined}
label="Sparkline"
color="text-yellow-400"
/>
</div>
{data?.llm.reqSpark?.length ? (
<div className="mt-2">
<Sparkline data={data.llm.reqSpark} color="#58a6ff" />
</div>
) : (
<p className="mt-2 text-center text-[10px] text-muted-foreground">
No traffic yet &mdash; send a prompt on{" "}
<a
className="text-blue-400 underline"
href="https://ai.asepharyana.my.id"
target="_blank"
rel="noopener noreferrer"
>
ai.asepharyana.my.id
</a>
</p>
)}
</CardContent>
</Card>
</motion.div>
{/* Request Rate */} {/* Request Rate */}
{hasTraffik && ( {hasTraffik && (
<motion.div variants={cardVariants}> <motion.div variants={cardVariants}>
+19
View File
@@ -30,6 +30,14 @@ export interface DashboardData {
errors: number[]; errors: number[];
traceVolume: number[]; traceVolume: number[];
links: { url: string; label: string }[]; 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 { export function safeDur(us: number): string {
@@ -55,3 +63,14 @@ export function serviceIndicator(state: string): string {
return "bg-red-400"; 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`;
}