Merge pull request #9225 from BerriAI/litellm_usage_tab_view

(UI Usage) - Allow clicking into Top Keys when showing users Top API Key
This commit is contained in:
Ishaan Jaff 2025-03-13 21:35:21 -07:00 committed by GitHub
commit 914409a058
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 256 additions and 16 deletions

View file

@ -0,0 +1,28 @@
import { KeyResponse } from "./key_list";
export const transformKeyInfo = (apiResponse: any): KeyResponse => {
const { key, info } = apiResponse;
return {
token: key,
key_name: info.key_name,
key_alias: info.key_alias,
spend: info.spend,
expires: info.expires,
models: info.models,
aliases: info.aliases,
config: info.config,
user_id: info.user_id,
team_id: info.team_id,
permissions: info.permissions,
max_parallel_requests: info.max_parallel_requests,
metadata: info.metadata,
tpm_limit: info.tpm_limit,
rpm_limit: info.rpm_limit,
max_budget: info.max_budget,
budget_duration: info.budget_duration,
organization_id: info.organization_id,
created_at: info.created_at,
litellm_budget_table: info.litellm_budget_table,
};
};

View file

@ -2273,6 +2273,37 @@ export const keyInfoCall = async (accessToken: String, keys: String[]) => {
}
};
export const keyInfoV1Call = async (accessToken: string, key: string) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/key/info` : `/key/info`;
url = `${url}?key=${key}`; // Add key as query parameter
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
// Remove body since this is a GET request
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch key info:", error);
throw error;
}
};
export const keyListCall = async (
accessToken: String,
organizationID: string | null,

View file

@ -0,0 +1,186 @@
import React, { useState } from "react";
import { BarChart } from "@tremor/react";
import KeyInfoView from "./key_info_view";
import { keyInfoV1Call } from "./networking";
import { transformKeyInfo } from "../components/key_team_helpers/transform_key_info";
import { DataTable } from "./view_logs/table";
import { Tooltip } from "antd";
import { Button } from "@tremor/react";
interface TopKeyViewProps {
topKeys: any[];
accessToken: string | null;
userID: string | null;
userRole: string | null;
teams: any[] | null;
}
const TopKeyView: React.FC<TopKeyViewProps> = ({
topKeys,
accessToken,
userID,
userRole,
teams
}) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [keyData, setKeyData] = useState<any | undefined>(undefined);
const [viewMode, setViewMode] = useState<'chart' | 'table'>('table');
const handleKeyClick = async (item: any) => {
if (!accessToken) return;
try {
const keyInfo = await keyInfoV1Call(accessToken, item.api_key);
const transformedKeyData = transformKeyInfo(keyInfo);
setKeyData(transformedKeyData);
setSelectedKey(item.key);
setIsModalOpen(true); // Open modal when key is clicked
} catch (error) {
console.error("Error fetching key info:", error);
}
};
const handleClose = () => {
setIsModalOpen(false);
setSelectedKey(null);
setKeyData(undefined);
};
// Handle clicking outside the modal
const handleOutsideClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
handleClose();
}
};
// Handle escape key
React.useEffect(() => {
const handleEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isModalOpen) {
handleClose();
}
};
document.addEventListener('keydown', handleEscapeKey);
return () => document.removeEventListener('keydown', handleEscapeKey);
}, [isModalOpen]);
// Define columns for the table view
const columns = [
{
header: "Key ID",
accessorKey: "api_key",
cell: (info: any) => (
<div className="overflow-hidden">
<Tooltip title={info.getValue() as string}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => handleKeyClick(info.row.original)}
>
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}
</Button>
</Tooltip>
</div>
),
},
{
header: "Key Alias",
accessorKey: "key_alias",
cell: (info: any) => info.getValue() || "-",
},
{
header: "Spend (USD)",
accessorKey: "spend",
cell: (info: any) => `$${Number(info.getValue()).toFixed(2)}`,
},
];
return (
<>
<div className="mb-4 flex justify-end items-center">
<div className="flex space-x-2">
<button
onClick={() => setViewMode('table')}
className={`px-3 py-1 text-sm rounded-md ${viewMode === 'table' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'}`}
>
Table View
</button>
<button
onClick={() => setViewMode('chart')}
className={`px-3 py-1 text-sm rounded-md ${viewMode === 'chart' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'}`}
>
Chart View
</button>
</div>
</div>
{viewMode === 'chart' ? (
<div className="relative">
<BarChart
className="mt-4 h-40 cursor-pointer hover:opacity-90"
data={topKeys}
index="key"
categories={["spend"]}
colors={["cyan"]}
yAxisWidth={80}
tickGap={5}
layout="vertical"
showXAxis={false}
showLegend={false}
valueFormatter={(value) => `$${value.toFixed(2)}`}
onValueChange={(item) => handleKeyClick(item)}
showTooltip={true}
/>
</div>
) : (
<div className="border rounded-lg overflow-hidden">
<DataTable
columns={columns}
data={topKeys}
renderSubComponent={() => <></>}
getRowCanExpand={() => false}
isLoading={false}
/>
</div>
)}
{isModalOpen && selectedKey && keyData && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={handleOutsideClick}
>
<div className="bg-white rounded-lg shadow-xl relative w-11/12 max-w-6xl max-h-[90vh] overflow-y-auto min-h-[750px]">
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 focus:outline-none"
aria-label="Close"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Content */}
<div className="p-6 h-full">
<KeyInfoView
keyId={selectedKey}
onClose={handleClose}
keyData={keyData}
accessToken={accessToken}
userID={userID}
userRole={userRole}
teams={teams}
/>
</div>
</div>
</div>
)}
</>
);
};
export default TopKeyView;

View file

@ -39,6 +39,7 @@ import {
getProxyUISettings
} from "./networking";
import { start } from "repl";
import TopKeyView from "./top_key_view";
console.log("process.env.NODE_ENV", process.env.NODE_ENV);
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
@ -399,7 +400,9 @@ const UsagePage: React.FC<UsagePageProps> = ({
async () => {
const top_keys = await adminTopKeysCall(accessToken);
return top_keys.map((k: any) => ({
key: (k["key_alias"] || k["key_name"] || k["api_key"]).substring(0, 10),
key: (k["api_key"]).substring(0, 10),
api_key: k["api_key"],
key_alias: k["key_alias"],
spend: Number(k["total_spend"].toFixed(2)),
}));
},
@ -654,25 +657,18 @@ const UsagePage: React.FC<UsagePageProps> = ({
</Card>
</Col>
<Col numColSpan={1}>
<Card>
<Card className="h-full">
<Title>Top API Keys</Title>
<BarChart
className="mt-4 h-40"
data={topKeys}
index="key"
categories={["spend"]}
colors={["cyan"]}
yAxisWidth={80}
tickGap={5}
layout="vertical"
showXAxis={false}
showLegend={false}
valueFormatter={(value) => `$${value.toFixed(2)}`}
<TopKeyView
topKeys={topKeys}
accessToken={accessToken}
userID={userID}
userRole={userRole}
/>
</Card>
</Col>
<Col numColSpan={1}>
<Card>
<Card className="h-full">
<Title>Top Models</Title>
<BarChart
className="mt-4 h-40"
@ -687,7 +683,6 @@ const UsagePage: React.FC<UsagePageProps> = ({
valueFormatter={(value) => `$${value.toFixed(2)}`}
/>
</Card>
</Col>
<Col numColSpan={1}>