// CRITICAL import type { Hono } from "hono"; import { performance } from "node:perf_hooks"; import type { AppContext } from "../types/context"; import { getGpuInfo } from "../services/gpu"; /** * Register monitoring routes. * @param app - Hono app. * @param context - App context. */ export const registerMonitoringRoutes = (app: Hono, context: AppContext): void => { app.get("/metrics", async (_ctx) => { const current = await context.processManager.findInferenceProcess(context.config.inference_port); if (current) { context.metrics.updateActiveModel(current.model_path, current.backend, current.served_model_name); } else { context.metrics.updateActiveModel(); } const gpus = getGpuInfo(); context.metrics.updateGpuMetrics(gpus.map((gpu) => ({ ...gpu }))); context.metrics.updateSseMetrics(context.eventManager.getStats()); const content = await context.metricsRegistry.getMetrics(); return new Response(content, { headers: { "Content-Type": context.metricsRegistry.contentType }, }); }); app.get("/peak-metrics", async (ctx) => { const modelId = ctx.req.query("model_id"); if (modelId) { const result = context.stores.peakMetricsStore.get(modelId); return ctx.json(result ?? { error: "No metrics for this model" }); } return ctx.json({ metrics: context.stores.peakMetricsStore.getAll() }); }); app.get("/lifetime-metrics", async (ctx) => { const data = context.stores.lifetimeMetricsStore.getAll(); const uptimeHours = (data["uptime_seconds"] ?? 0) / 3600; const energyKwh = (data["energy_wh"] ?? 0) / 1000; const tokens = data["tokens_total"] ?? 0; const kwhPerMillion = tokens > 0 ? energyKwh / (tokens / 1_000_000) : 0; const gpus = getGpuInfo(); const currentPower = gpus.reduce((sum, gpu) => sum + gpu.power_draw, 0); return ctx.json({ tokens_total: Math.floor(data["tokens_total"] ?? 0), requests_total: Math.floor(data["requests_total"] ?? 0), energy_wh: data["energy_wh"] ?? 0, energy_kwh: energyKwh, uptime_seconds: data["uptime_seconds"] ?? 0, uptime_hours: uptimeHours, first_started_at: data["first_started_at"] ?? 0, kwh_per_million_tokens: kwhPerMillion, current_power_watts: currentPower, }); }); app.post("/benchmark", async (ctx) => { const promptTokens = Number(ctx.req.query("prompt_tokens") ?? 1000); const maxTokens = Number(ctx.req.query("max_tokens") ?? 100); const current = await context.processManager.findInferenceProcess(context.config.inference_port); if (!current) { return ctx.json({ error: "No model running" }); } const modelId = current.served_model_name ?? current.model_path?.split("/").pop() ?? "unknown"; const prompt = `Please count: ${Array.from({ length: Math.floor(promptTokens / 2) }).map((_, index) => index.toString()).join(" ")}`; try { const start = performance.now(); const response = await fetch(`http://localhost:${context.config.inference_port}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: modelId, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens, stream: false, }), }); const totalTime = (performance.now() - start) / 1000; if (!response.ok) { return ctx.json({ error: `Request failed: ${response.status}` }); } const data = (await response.json()) as { usage?: Record }; const usage = data.usage ?? {}; const promptTokensActual = usage["prompt_tokens"] ?? 0; const completionTokens = usage["completion_tokens"] ?? 0; if (completionTokens > 0 && promptTokensActual > 0) { // Calculate generation throughput from total time // Note: This includes prefill time so it's a conservative estimate // Real-time metrics collector tracks actual generation throughput more accurately const generationTps = completionTokens / totalTime; // Don't fake prefill - it requires TTFT measurement from streaming const result = context.stores.peakMetricsStore.updateIfBetter( modelId, undefined, // prefill requires proper TTFT measurement generationTps, undefined, // TTFT requires streaming measurement ); context.stores.peakMetricsStore.addTokens(modelId, completionTokens, 1); return ctx.json({ success: true, model_id: modelId, benchmark: { prompt_tokens: promptTokensActual, completion_tokens: completionTokens, total_time_s: Math.round(totalTime * 100) / 100, generation_tps: Math.round(generationTps * 10) / 10, }, peak_metrics: result, }); } return ctx.json({ error: "No tokens in response" }); } catch (error) { return ctx.json({ error: String(error) }); } }); };