- {premiumUser ? null : (
-
- )}
+ <>
+
+
+ >
);
};
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 744442ec7c..30a84f4760 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -1513,6 +1513,54 @@ export const userSpendLogsCall = async (
throw error;
}
};
+export const uiSpendLogsCall = async (
+ accessToken: String,
+ api_key?: string,
+ user_id?: string,
+ request_id?: string,
+ start_date?: string,
+ end_date?: string,
+) => {
+ try {
+ // Construct base URL
+ let url = proxyBaseUrl ? `${proxyBaseUrl}/spend/logs/ui` : `/spend/logs/ui`;
+
+ // Add query parameters if they exist
+ const queryParams = new URLSearchParams();
+ if (api_key) queryParams.append('api_key', api_key);
+ if (user_id) queryParams.append('user_id', user_id);
+ if (request_id) queryParams.append('request_id', request_id);
+ if (start_date) queryParams.append('start_date', start_date);
+ if (end_date) queryParams.append('end_date', end_date);
+ // Append query parameters to URL if any exist
+ const queryString = queryParams.toString();
+ if (queryString) {
+ url += `?${queryString}`;
+ }
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response.text();
+ handleError(errorData);
+ throw new Error("Network response was not ok");
+ }
+
+ const data = await response.json();
+ console.log("Spend Logs UI Response:", data);
+ return data;
+ } catch (error) {
+ console.error("Failed to fetch spend logs:", error);
+ throw error;
+ }
+};
+
export const adminSpendLogsCall = async (accessToken: String) => {
try {
diff --git a/ui/litellm-dashboard/src/components/view_logs.tsx b/ui/litellm-dashboard/src/components/view_logs.tsx
new file mode 100644
index 0000000000..097ef9a172
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs.tsx
@@ -0,0 +1,363 @@
+import React, { useEffect, useState } from 'react';
+import { Card } from "@tremor/react";
+import {
+ Table,
+ TableHead,
+ TableHeaderCell,
+ TableBody,
+ TableRow,
+ TableCell,
+ Text
+} from "@tremor/react";
+import { uiSpendLogsCall } from './networking';
+import moment from 'moment';
+import {
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+ getSortedRowModel,
+ SortingState,
+} from '@tanstack/react-table';
+
+interface SpendLogsTableProps {
+ accessToken: string | null;
+ token: string | null;
+ userRole: string | null;
+ userID: string | null;
+}
+
+interface LogEntry {
+ request_id: string;
+ api_key: string;
+ model: string;
+ api_base?: string;
+ call_type: string;
+ spend: number;
+ total_tokens: number;
+ prompt_tokens: number;
+ completion_tokens: number;
+ startTime: string;
+ endTime: string;
+ user?: string;
+ metadata?: Record
;
+ cache_hit: string;
+ cache_key?: string;
+ request_tags?: Record;
+ requester_ip_address?: string;
+ messages: string | any[] | Record;
+ response: string | any[] | Record;
+}
+
+const formatMessage = (message: any): string => {
+ if (!message) return 'N/A';
+ if (typeof message === 'string') return message;
+ if (typeof message === 'object') {
+ // Handle the {text, type} object specifically
+ if (message.text) return message.text;
+ if (message.content) return message.content;
+ return JSON.stringify(message);
+ }
+ return String(message);
+};
+
+const RequestViewer: React.FC<{ data: any }> = ({ data }) => {
+ const formatData = (input: any) => {
+ if (typeof input === 'string') {
+ try {
+ return JSON.parse(input);
+ } catch {
+ return input;
+ }
+ }
+ return input;
+ };
+
+ return (
+
+ {/* Request Card */}
+
+
+
Request
+
+
+
+
+
+
+ {JSON.stringify(formatData(data.request), null, 2)}
+
+
+
+
+ {/* Response Card */}
+
+
+
Response
+
+
+
+
+
+
+ {JSON.stringify(formatData(data.response), null, 2)}
+
+
+ {/* Metadata Card */}
+ {data.metadata && Object.keys(data.metadata).length > 0 && (
+
+
+
+ {JSON.stringify(data.metadata, null, 2)}
+
+
+ )}
+
+ );
+};
+
+export const SpendLogsTable: React.FC = ({ accessToken, token, userRole, userID }) => {
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [sorting, setSorting] = useState([]);
+ const [expandedRow, setExpandedRow] = useState(null);
+
+ const columnHelper = createColumnHelper();
+
+ const columns = [
+ {
+ header: 'Time',
+ accessorKey: 'startTime',
+ cell: (info: any) => (
+ {moment(info.getValue()).format('MMM DD HH:mm:ss')}
+ ),
+ },
+ {
+ header: 'Request ID',
+ accessorKey: 'request_id',
+ cell: (info: any) => (
+ {String(info.getValue() || '')}
+ ),
+ },
+ {
+ header: 'Type',
+ accessorKey: 'call_type',
+ cell: (info: any) => (
+ {String(info.getValue() || '')}
+ ),
+ },
+ {
+ header: 'Request',
+ accessorKey: 'messages',
+ cell: (info: any) => {
+ const messages = info.getValue();
+ try {
+ const content = typeof messages === 'string' ? JSON.parse(messages) : messages;
+ let displayText = '';
+
+ if (Array.isArray(content)) {
+ displayText = formatMessage(content[0]?.content);
+ } else {
+ displayText = formatMessage(content);
+ }
+
+ return {displayText};
+ } catch (e) {
+ return {formatMessage(messages)};
+ }
+ },
+ },
+ {
+ header: 'Model',
+ accessorKey: 'model',
+ cell: (info: any) => {String(info.getValue() || '')},
+ },
+ {
+ header: 'Tokens',
+ accessorKey: 'total_tokens',
+ cell: (info: any) => {
+ const row = info.row.original;
+ return (
+
+ {String(row.total_tokens || '0')}
+
+ ({String(row.prompt_tokens || '0')}+{String(row.completion_tokens || '0')})
+
+
+ );
+ },
+ },
+ {
+ header: 'User',
+ accessorKey: 'user',
+ cell: (info: any) => {String(info.getValue() || '-')},
+ },
+ {
+ header: 'Cost',
+ accessorKey: 'spend',
+ cell: (info: any) => ${(Number(info.getValue() || 0)).toFixed(6)},
+ },
+ {
+ header: 'Tags',
+ accessorKey: 'request_tags',
+ cell: (info: any) => {
+ const tags = info.getValue();
+ if (!tags || Object.keys(tags).length === 0) return '-';
+ return (
+
+ {Object.entries(tags).map(([key, value]) => (
+
+ {key}: {String(value)}
+
+ ))}
+
+ );
+ },
+ },
+ ];
+
+ const table = useReactTable({
+ data: logs,
+ columns,
+ state: {
+ sorting,
+ },
+ onSortingChange: setSorting,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ });
+
+ useEffect(() => {
+ const fetchLogs = async () => {
+ if (!accessToken || !token || !userRole || !userID) {
+ console.log("got None values for one of accessToken, token, userRole, userID");
+ return;
+ }
+
+ try {
+ setLoading(true);
+ // Get logs for last 24 hours using ISO string and proper date formatting
+ const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
+ const startTime = moment().subtract(24, 'hours').format('YYYY-MM-DD HH:mm:ss');
+
+ const data = await uiSpendLogsCall(
+ accessToken,
+ token,
+ userRole,
+ userID,
+ startTime,
+ endTime
+ );
+
+ console.log("response from uiSpendLogsCall:", data);
+
+ // Transform the data and add unique keys
+ const formattedLogs = data.map((log: LogEntry, index: number) => ({
+ ...log,
+ key: index.toString(),
+ }));
+
+ setLogs(formattedLogs);
+ } catch (error) {
+ console.error('Error fetching logs:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchLogs();
+ }, [accessToken, token, userRole, userID]);
+
+ if (!accessToken || !token || !userRole || !userID) {
+ console.log("got None values for one of accessToken, token, userRole, userID");
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {columns.map((column) => (
+
+ {column.header}
+
+ ))}
+
+
+
+ {logs.map((log) => (
+
+ setExpandedRow(expandedRow === log.request_id ? null : log.request_id)}
+ >
+ {columns.map((column) => (
+
+ {column.cell ?
+ column.cell({
+ getValue: () => log[column.accessorKey as keyof LogEntry],
+ row: { original: log }
+ }) :
+ {String(log[column.accessorKey as keyof LogEntry] ?? '')}
+ }
+
+ ))}
+
+ {expandedRow === log.request_id && (
+
+
+
+
+
+ )}
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default SpendLogsTable;
\ No newline at end of file