mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
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:
commit
914409a058
4 changed files with 256 additions and 16 deletions
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
186
ui/litellm-dashboard/src/components/top_key_view.tsx
Normal file
186
ui/litellm-dashboard/src/components/top_key_view.tsx
Normal 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;
|
||||
|
|
@ -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}>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue