mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
UI fixes
This commit is contained in:
parent
547a5815fc
commit
bc15f99940
2 changed files with 0 additions and 415 deletions
|
|
@ -1,222 +0,0 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Row } from "@tanstack/react-table";
|
||||
import { Tooltip } from "antd";
|
||||
import { TableRow, TableCell } from "@tremor/react";
|
||||
import { getSpendString } from "@/utils/dataUtils";
|
||||
import { sessionSpendLogsCall } from "../networking";
|
||||
import type { LogEntry } from "./columns";
|
||||
import { TimeCell } from "./time_cell";
|
||||
import { getProviderLogoAndName } from "../provider_info_helpers";
|
||||
|
||||
interface SessionChildRowsProps {
|
||||
row: Row<LogEntry>;
|
||||
accessToken: string;
|
||||
onChildClick?: (log: LogEntry) => void;
|
||||
}
|
||||
|
||||
const MCP_CALL_TYPES = ["call_mcp_tool", "list_mcp_tools"];
|
||||
|
||||
const LlmBadge = () => (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 border border-blue-200 rounded-full text-[11px] font-medium whitespace-nowrap">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0 text-gray-400">
|
||||
<path d="M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3z" />
|
||||
</svg>
|
||||
LLM
|
||||
</span>
|
||||
);
|
||||
|
||||
const McpBadge = () => (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-50 text-amber-700 border border-amber-200 rounded-full text-[11px] font-medium whitespace-nowrap">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
MCP
|
||||
</span>
|
||||
);
|
||||
|
||||
export function SessionChildRows({ row, accessToken, onChildClick }: SessionChildRowsProps) {
|
||||
const sessionId = row.original.session_id;
|
||||
|
||||
const { data: children, isLoading } = useQuery({
|
||||
queryKey: ["sessionChildren", sessionId],
|
||||
queryFn: async () => {
|
||||
if (!sessionId) return [];
|
||||
const response = await sessionSpendLogsCall(accessToken, sessionId);
|
||||
const allLogs: LogEntry[] = response.data || response || [];
|
||||
// Sort chronologically
|
||||
return allLogs.sort((a, b) =>
|
||||
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
|
||||
);
|
||||
},
|
||||
enabled: !!sessionId && !!accessToken,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableCell colSpan={99} className="py-2 pl-12 text-sm text-gray-400">
|
||||
Loading session calls...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
return (
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableCell colSpan={99} className="py-2 pl-12 text-sm text-gray-400">
|
||||
No calls found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children.map((child) => {
|
||||
const isMcp = MCP_CALL_TYPES.includes(child.call_type);
|
||||
const modelOrTool = isMcp
|
||||
? (child.model?.replace("MCP: ", "") || "unknown")
|
||||
: (child.model || "-");
|
||||
const serverName = isMcp
|
||||
? (child.metadata?.mcp_tool_call_metadata?.mcp_server_name || "")
|
||||
: "";
|
||||
const provider = child.custom_llm_provider || "";
|
||||
const logoUrl = isMcp
|
||||
? (child.metadata?.mcp_tool_call_metadata?.mcp_server_logo_url || "")
|
||||
: (provider ? getProviderLogoAndName(provider).logo : "");
|
||||
const duration =
|
||||
child.startTime && child.endTime
|
||||
? ((Date.parse(child.endTime) - Date.parse(child.startTime)) / 1000).toFixed(3)
|
||||
: "-";
|
||||
const status = (child.metadata?.status || "Success").toLowerCase();
|
||||
const isSuccess = status !== "failure";
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={child.request_id}
|
||||
className="h-8 bg-gray-50 cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => onChildClick?.(child)}
|
||||
>
|
||||
{/* Expander: branch connector */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden pl-3">
|
||||
<div className="flex items-center justify-center text-gray-400">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-indigo-400">
|
||||
<path d="M4 0 L4 8 L12 8" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||||
<path d="M10 5.5 L12.5 8 L10 10.5" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||||
</svg>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Time */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<TimeCell utcTime={child.startTime} />
|
||||
</TableCell>
|
||||
|
||||
{/* Type */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{isMcp ? <McpBadge /> : <LlmBadge />}
|
||||
</TableCell>
|
||||
|
||||
{/* Status */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 rounded-md text-xs font-medium inline-block text-center w-16 ${
|
||||
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{isSuccess ? "Success" : "Failure"}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Session ID */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="font-mono text-xs max-w-[15ch] truncate block text-gray-400">
|
||||
{child.session_id || ""}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Request ID */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<Tooltip title={child.request_id}>
|
||||
<span className="font-mono text-xs max-w-[15ch] truncate block">{child.request_id}</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
|
||||
{/* Cost */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span>{getSpendString(child.spend || 0)}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Duration */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span>{duration}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Team Name */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="max-w-[15ch] truncate block">
|
||||
{child.metadata?.user_api_key_team_alias || "-"}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Key Hash */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="font-mono max-w-[15ch] truncate block">
|
||||
{child.metadata?.user_api_key || "-"}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Key Name */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="max-w-[15ch] truncate block">
|
||||
{child.metadata?.user_api_key_alias || "-"}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Model / Tool */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div className="flex items-center space-x-2">
|
||||
{logoUrl && (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt=""
|
||||
className="w-4 h-4"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
)}
|
||||
<Tooltip title={`${modelOrTool}${serverName ? ` (${serverName})` : ""}`}>
|
||||
<span className="max-w-[15ch] truncate block font-semibold">{modelOrTool}</span>
|
||||
</Tooltip>
|
||||
{serverName && (
|
||||
<span className="text-xs text-gray-400">{serverName}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Tokens */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="text-sm">{child.total_tokens || "0"}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Internal User */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="max-w-[15ch] truncate block">{child.user || "-"}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* End User */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="max-w-[15ch] truncate block">{child.end_user || "-"}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* Tags */}
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span>-</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import { LogEntry } from "./columns";
|
||||
import { DataTable } from "./table";
|
||||
import { columns } from "./columns";
|
||||
import { Card, Title, Text, Metric, Button as TremorButton } from "@tremor/react";
|
||||
import { RequestViewer } from "./index";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { Button } from "antd";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { Tooltip } from "antd";
|
||||
|
||||
interface SessionViewProps {
|
||||
sessionId: string;
|
||||
logs: LogEntry[];
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const SessionView: React.FC<SessionViewProps> = ({ sessionId, logs, onBack }) => {
|
||||
// Track which log row is expanded
|
||||
const [expandedRequestId, setExpandedRequestId] = useState<string | null>(null);
|
||||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Calculate session metrics
|
||||
const totalCost = logs.reduce((sum, log) => sum + (log.spend || 0), 0);
|
||||
const totalTokens = logs.reduce((sum, log) => sum + (log.total_tokens || 0), 0);
|
||||
|
||||
// Calculate cache token totals from metadata
|
||||
const totalCacheReadTokens = logs.reduce((sum, log) => {
|
||||
const cacheReadTokens = log.metadata?.additional_usage_values?.cache_read_input_tokens || 0;
|
||||
return sum + cacheReadTokens;
|
||||
}, 0);
|
||||
|
||||
const totalCacheCreationTokens = logs.reduce((sum, log) => {
|
||||
const cacheCreationTokens = log.metadata?.additional_usage_values?.cache_creation_input_tokens || 0;
|
||||
return sum + cacheCreationTokens;
|
||||
}, 0);
|
||||
|
||||
// Calculate total tokens including cache tokens
|
||||
const totalTokensWithCache = totalTokens + totalCacheReadTokens + totalCacheCreationTokens;
|
||||
|
||||
const startTime = logs.length > 0 ? new Date(logs[0].startTime) : new Date();
|
||||
const endTime = logs.length > 0 ? new Date(logs[logs.length - 1].endTime) : new Date();
|
||||
const durationMs = endTime.getTime() - startTime.getTime();
|
||||
const durationSec = (durationMs / 1000).toFixed(2);
|
||||
|
||||
// Prepare data for the timeline chart
|
||||
const timelineData = logs.map((log) => ({
|
||||
time: new Date(log.startTime).toISOString(),
|
||||
tokens: log.total_tokens || 0,
|
||||
cost: log.spend || 0,
|
||||
}));
|
||||
|
||||
const copyToClipboard = async (text: string, key: string) => {
|
||||
const success = await utilCopyToClipboard(text);
|
||||
if (success) {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: true }));
|
||||
setTimeout(() => {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: false }));
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header with back button */}
|
||||
<div className="mb-8">
|
||||
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onBack} className="mb-4">
|
||||
Back to All Logs
|
||||
</TremorButton>
|
||||
<div className="mt-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Session Details</h1>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<p className="text-sm text-gray-500 font-mono">{sessionId}</p>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["session-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(sessionId, "session-id")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["session-id"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/ui_logs_sessions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
|
||||
>
|
||||
Get started with session management here
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Session Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<Text>Total Requests</Text>
|
||||
<Metric>{logs.length}</Metric>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Cost</Text>
|
||||
<Metric>${formatNumberWithCommas(totalCost, 6)}</Metric>
|
||||
</Card>
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="text-white min-w-[200px]">
|
||||
<div className="text-lg font-medium mb-3">Usage breakdown</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">Input usage:</div>
|
||||
<div className="space-y-2 text-sm text-gray-300">
|
||||
<div className="flex justify-between">
|
||||
<span>input:</span>
|
||||
<span className="ml-8">
|
||||
{formatNumberWithCommas(logs.reduce((sum, log) => sum + (log.prompt_tokens || 0), 0))}
|
||||
</span>
|
||||
</div>
|
||||
{totalCacheReadTokens > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span>input_cached_tokens:</span>
|
||||
<span className="ml-8">{formatNumberWithCommas(totalCacheReadTokens)}</span>
|
||||
</div>
|
||||
)}
|
||||
{totalCacheCreationTokens > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span>input_cache_creation_tokens:</span>
|
||||
<span className="ml-8">{formatNumberWithCommas(totalCacheCreationTokens)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 pt-3">
|
||||
<div className="text-base font-medium mb-2">Output usage:</div>
|
||||
<div className="space-y-2 text-sm text-gray-300">
|
||||
<div className="flex justify-between">
|
||||
<span>output:</span>
|
||||
<span className="ml-8">
|
||||
{formatNumberWithCommas(logs.reduce((sum, log) => sum + (log.completion_tokens || 0), 0))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 pt-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-medium">Total usage:</span>
|
||||
<span className="text-sm text-gray-300">{formatNumberWithCommas(totalTokensWithCache)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
overlayStyle={{ minWidth: "300px" }}
|
||||
>
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text>Total Tokens</Text>
|
||||
<span className="text-gray-400 text-sm">ⓘ</span>
|
||||
</div>
|
||||
<Metric>{formatNumberWithCommas(totalTokensWithCache)}</Metric>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* Request Timeline */}
|
||||
<Title>Session Logs</Title>
|
||||
<div className="mt-4">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={logs}
|
||||
renderSubComponent={RequestViewer}
|
||||
getRowCanExpand={() => true}
|
||||
loadingMessage="Loading logs..."
|
||||
noDataMessage="No logs found"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue