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:
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
</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 — 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 */}
|
||||
{hasTraffik && (
|
||||
<motion.div variants={cardVariants}>
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user