diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/transform_key_info.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/transform_key_info.tsx new file mode 100644 index 00000000000..49e248e2bc4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/key_team_helpers/transform_key_info.tsx @@ -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, + }; +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 46099906ac3..19589400a0e 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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, diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx new file mode 100644 index 00000000000..d687e499d36 --- /dev/null +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -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 = ({ + topKeys, + accessToken, + userID, + userRole, + teams +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedKey, setSelectedKey] = useState(null); + const [keyData, setKeyData] = useState(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) => { + 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) => ( +
+ + + +
+ ), + }, + { + 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 ( + <> +
+
+ + +
+
+ + {viewMode === 'chart' ? ( +
+ `$${value.toFixed(2)}`} + onValueChange={(item) => handleKeyClick(item)} + showTooltip={true} + /> +
+ ) : ( +
+ <>} + getRowCanExpand={() => false} + isLoading={false} + /> +
+ )} + + {isModalOpen && selectedKey && keyData && ( +
+
+ {/* Close button */} + + + {/* Content */} +
+ +
+
+
+ )} + + ); +}; + +export default TopKeyView; diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index 053f69c256f..e12ba244c18 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -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 = ({ 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 = ({ - + Top API Keys - `$${value.toFixed(2)}`} + - + Top Models = ({ valueFormatter={(value) => `$${value.toFixed(2)}`} /> -