Add metrics logging for chat completion requests (#195)

- Add token and performance metrics  for v1/chat/completions 
- Add Activity Page in UI
- Add /api/metrics endpoint

Contributed by @g2mt
This commit is contained in:
g2mt
2025-07-21 22:19:55 -07:00
committed by GitHub
parent 307e619521
commit 87dce5f8f6
15 changed files with 576 additions and 31 deletions

View File

@@ -3,6 +3,7 @@ import { useTheme } from "./contexts/ThemeProvider";
import { APIProvider } from "./contexts/APIProvider";
import LogViewerPage from "./pages/LogViewer";
import ModelPage from "./pages/Models";
import ActivityPage from "./pages/Activity";
function App() {
const theme = useTheme();
@@ -21,6 +22,10 @@ function App() {
<NavLink to="/models" className={({ isActive }) => (isActive ? "navlink active" : "navlink")}>
Models
</NavLink>
<NavLink to="/activity" className={({ isActive }) => (isActive ? "navlink active" : "navlink")}>
Activity
</NavLink>
<button className="btn btn--sm" onClick={theme.toggleTheme}>
{theme.isDarkMode ? "🌙" : "☀️"}
</button>
@@ -32,6 +37,7 @@ function App() {
<Routes>
<Route path="/" element={<LogViewerPage />} />
<Route path="/models" element={<ModelPage />} />
<Route path="/activity" element={<ActivityPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>

View File

@@ -19,13 +19,25 @@ interface APIProviderType {
enableAPIEvents: (enabled: boolean) => void;
proxyLogs: string;
upstreamLogs: string;
metrics: Metrics[];
}
interface Metrics {
id: number;
timestamp: string;
model: string;
input_tokens: number;
output_tokens: number;
tokens_per_second: number;
duration_ms: number;
}
interface LogData {
source: "upstream" | "proxy";
data: string;
}
interface APIEventEnvelope {
type: "modelStatus" | "logData";
type: "modelStatus" | "logData" | "metrics";
data: string;
}
@@ -37,6 +49,7 @@ type APIProviderProps = {
export function APIProvider({ children }: APIProviderProps) {
const [proxyLogs, setProxyLogs] = useState("");
const [upstreamLogs, setUpstreamLogs] = useState("");
const [metrics, setMetrics] = useState<Metrics[]>([]);
const apiEventSource = useRef<EventSource | null>(null);
const [models, setModels] = useState<Model[]>([]);
@@ -73,7 +86,7 @@ export function APIProvider({ children }: APIProviderProps) {
}
break;
case "logData": {
case "logData":
const logData = JSON.parse(message.data) as LogData;
switch (logData.source) {
case "proxy":
@@ -83,7 +96,16 @@ export function APIProvider({ children }: APIProviderProps) {
appendLog(logData.data, setUpstreamLogs);
break;
}
}
break;
case "metrics":
{
const newMetric = JSON.parse(message.data) as Metrics;
setMetrics(prevMetrics => {
return [newMetric, ...prevMetrics];
});
}
break;
}
} catch (err) {
console.error(e.data, err);
@@ -159,8 +181,9 @@ export function APIProvider({ children }: APIProviderProps) {
enableAPIEvents,
proxyLogs,
upstreamLogs,
metrics,
}),
[models, listModels, unloadAllModels, loadModel, enableAPIEvents, proxyLogs, upstreamLogs]
[models, listModels, unloadAllModels, loadModel, enableAPIEvents, proxyLogs, upstreamLogs, metrics]
);
return <APIContext.Provider value={value}>{children}</APIContext.Provider>;

108
ui/src/pages/Activity.tsx Normal file
View File

@@ -0,0 +1,108 @@
import { useState, useEffect } from 'react';
import { useAPI } from '../contexts/APIProvider';
const ActivityPage = () => {
const { metrics, enableAPIEvents } = useAPI();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
enableAPIEvents(true);
return () => {
enableAPIEvents(false);
};
}, []);
useEffect(() => {
if (metrics.length > 0) {
setError(null);
}
}, [metrics]);
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString();
};
const formatSpeed = (speed: number) => {
return speed.toFixed(2) + ' t/s';
};
const formatDuration = (ms: number) => {
return (ms / 1000).toFixed(2) + 's';
};
if (error) {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Activity</h1>
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-800">{error}</p>
</div>
</div>
);
}
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Activity</h1>
{metrics.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-600">No metrics data available</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y">
<thead>
<tr>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Timestamp
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Model
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Input Tokens
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Output Tokens
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Processing Speed
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Duration
</th>
</tr>
</thead>
<tbody className="divide-y">
{metrics.map((metric, index) => (
<tr key={`${metric.id}-${index}`}>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{formatTimestamp(metric.timestamp)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{metric.model}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{metric.input_tokens.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{metric.output_tokens.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{formatSpeed(metric.tokens_per_second)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{formatDuration(metric.duration_ms)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};
export default ActivityPage;