- handleImageError(e, providerDisplayName)}
/>
diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx b/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx
index c356982f189..32ffd55efa0 100644
--- a/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx
+++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx
@@ -1,16 +1,16 @@
-import React, { useState, useEffect, useCallback } from "react";
-import { Title, Text, Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
+import React, { useState, useEffect } from "react";
+import { Title, Text, Button, Accordion, AccordionHeader, AccordionBody, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { Modal, Form } from "antd";
-import { getProxyBaseUrl } from "@/components/networking";
-import NotificationsManager from "../molecules/notifications_manager";
-import { Providers } from "../provider_info_helpers";
-import { CostTrackingSettingsProps, DiscountConfig } from "./types";
-import { getProviderBackendValue } from "./provider_display_helpers";
+import { CostTrackingSettingsProps } from "./types";
import ProviderDiscountTable from "./provider_discount_table";
import AddProviderForm from "./add_provider_form";
+import ProviderMarginTable from "./provider_margin_table";
+import AddMarginForm from "./add_margin_form";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { DocsMenu } from "../HelpLink";
import HowItWorks from "./how_it_works";
+import { useDiscountConfig } from "./use_discount_config";
+import { useMarginConfig } from "./use_margin_config";
const DOCS_LINKS = [
{ label: "Custom pricing for models", href: "https://docs.litellm.ai/docs/proxy/custom_pricing" },
@@ -22,118 +22,51 @@ const CostTrackingSettings: React.FC = ({
userRole,
accessToken
}) => {
- const [discountConfig, setDiscountConfig] = useState({});
const [selectedProvider, setSelectedProvider] = useState(undefined);
const [newDiscount, setNewDiscount] = useState("");
const [isFetching, setIsFetching] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
+ const [isMarginModalVisible, setIsMarginModalVisible] = useState(false);
+ const [selectedMarginProvider, setSelectedMarginProvider] = useState(undefined);
+ const [marginType, setMarginType] = useState<"percentage" | "fixed">("percentage");
+ const [percentageValue, setPercentageValue] = useState("");
+ const [fixedAmountValue, setFixedAmountValue] = useState("");
const [form] = Form.useForm();
+ const [marginForm] = Form.useForm();
const [modal, contextHolder] = Modal.useModal();
- const fetchDiscountConfig = useCallback(async () => {
- setIsFetching(true);
- try {
- const proxyBaseUrl = getProxyBaseUrl();
- const url = proxyBaseUrl
- ? `${proxyBaseUrl}/config/cost_discount_config`
- : "/config/cost_discount_config";
-
- const response = await fetch(url, {
- method: "GET",
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json",
- },
- });
+ // Use custom hooks for discount and margin config
+ const {
+ discountConfig,
+ fetchDiscountConfig,
+ handleAddProvider: addProvider,
+ handleRemoveProvider: removeProvider,
+ handleDiscountChange,
+ } = useDiscountConfig({ accessToken });
- if (response.ok) {
- const data = await response.json();
- setDiscountConfig(data.values || {});
- } else {
- console.error("Failed to fetch discount config");
- }
- } catch (error) {
- console.error("Error fetching discount config:", error);
- NotificationsManager.fromBackend("Failed to fetch discount configuration");
- } finally {
- setIsFetching(false);
- }
- }, [accessToken]);
+ const {
+ marginConfig,
+ fetchMarginConfig,
+ handleAddMargin: addMargin,
+ handleRemoveMargin: removeMargin,
+ handleMarginChange,
+ } = useMarginConfig({ accessToken });
useEffect(() => {
if (accessToken) {
- fetchDiscountConfig();
- }
- }, [accessToken, fetchDiscountConfig]);
-
- const saveDiscountConfig = async (config: DiscountConfig) => {
- try {
- const proxyBaseUrl = getProxyBaseUrl();
- const url = proxyBaseUrl
- ? `${proxyBaseUrl}/config/cost_discount_config`
- : "/config/cost_discount_config";
-
- const response = await fetch(url, {
- method: "PATCH",
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify(config),
+ Promise.all([fetchDiscountConfig(), fetchMarginConfig()]).finally(() => {
+ setIsFetching(false);
});
-
- if (response.ok) {
- NotificationsManager.success("Discount configuration updated successfully");
- await fetchDiscountConfig();
- } else {
- const errorData = await response.json();
- const errorMessage = errorData.detail?.error || errorData.detail || "Failed to update settings";
- NotificationsManager.fromBackend(errorMessage);
- }
- } catch (error) {
- console.error("Error updating discount config:", error);
- NotificationsManager.fromBackend("Failed to update discount configuration");
}
- };
+ }, [accessToken, fetchDiscountConfig, fetchMarginConfig]);
const handleAddProvider = async () => {
- if (!selectedProvider || !newDiscount) {
- NotificationsManager.fromBackend("Please select a provider and enter discount percentage");
- return;
+ const success = await addProvider(selectedProvider, newDiscount);
+ if (success) {
+ setSelectedProvider(undefined);
+ setNewDiscount("");
+ setIsModalVisible(false);
}
-
- const percentageValue = parseFloat(newDiscount);
- if (isNaN(percentageValue) || percentageValue < 0 || percentageValue > 100) {
- NotificationsManager.fromBackend("Discount must be between 0% and 100%");
- return;
- }
-
- const providerValue = getProviderBackendValue(selectedProvider);
-
- if (!providerValue) {
- NotificationsManager.fromBackend("Invalid provider selected");
- return;
- }
-
- if (discountConfig[providerValue]) {
- NotificationsManager.fromBackend(
- `Discount for ${Providers[selectedProvider as keyof typeof Providers]} already exists. Edit it in the table above.`
- );
- return;
- }
-
- // Convert percentage to decimal for storage
- const discountValue = percentageValue / 100;
- const updatedConfig = {
- ...discountConfig,
- [providerValue]: discountValue,
- };
-
- setDiscountConfig(updatedConfig);
- await saveDiscountConfig(updatedConfig);
- setSelectedProvider(undefined);
- setNewDiscount("");
- setIsModalVisible(false);
};
const handleModalCancel = () => {
@@ -143,7 +76,7 @@ const CostTrackingSettings: React.FC = ({
setNewDiscount("");
};
- const handleFormSubmit = (values: any) => {
+ const handleFormSubmit = () => {
handleAddProvider();
};
@@ -155,27 +88,47 @@ const CostTrackingSettings: React.FC = ({
okText: 'Remove',
okType: 'danger',
cancelText: 'Cancel',
- onOk: async () => {
- const updatedConfig = { ...discountConfig };
- delete updatedConfig[provider];
- setDiscountConfig(updatedConfig);
- await saveDiscountConfig(updatedConfig);
- },
+ onOk: () => removeProvider(provider),
});
};
- const handleDiscountChange = async (provider: string, value: string) => {
- const discountValue = parseFloat(value);
- if (!isNaN(discountValue) && discountValue >= 0 && discountValue <= 1) {
- const updatedConfig = {
- ...discountConfig,
- [provider]: discountValue,
- };
- setDiscountConfig(updatedConfig);
- await saveDiscountConfig(updatedConfig);
+ const handleAddMargin = async () => {
+ const success = await addMargin({
+ selectedProvider: selectedMarginProvider,
+ marginType,
+ percentageValue,
+ fixedAmountValue,
+ });
+ if (success) {
+ setSelectedMarginProvider(undefined);
+ setPercentageValue("");
+ setFixedAmountValue("");
+ setMarginType("percentage");
+ setIsMarginModalVisible(false);
}
};
+ const handleMarginModalCancel = () => {
+ setIsMarginModalVisible(false);
+ marginForm.resetFields();
+ setSelectedMarginProvider(undefined);
+ setPercentageValue("");
+ setFixedAmountValue("");
+ setMarginType("percentage");
+ };
+
+ const handleRemoveMargin = async (provider: string, providerDisplayName: string) => {
+ modal.confirm({
+ title: 'Remove Provider Margin',
+ icon: ,
+ content: `Are you sure you want to remove the margin for ${providerDisplayName}?`,
+ okText: 'Remove',
+ okType: 'danger',
+ cancelText: 'Cancel',
+ onOk: () => removeMargin(provider),
+ });
+ };
+
if (!accessToken) {
return null;
}
@@ -192,38 +145,113 @@ const CostTrackingSettings: React.FC = ({
- Configure cost discounts for different LLM providers. Changes are saved automatically.
+ Configure cost discounts and margins for different LLM providers. Changes are saved automatically.
-
- {/* Main Content Card with Tabs */}
-
-
-
- Provider Discounts
- Test It
-
-
-
+ {/* Main Content Card with Accordions */}
+
+ {/* Accordion 1: Provider Discounts */}
+
+
+
+ Provider Discounts
+
+ Apply percentage-based discounts to reduce costs for specific providers
+
+
+
+
+
+
+ Discounts
+ Test It
+
+
+
+
+
+
+
+ {isFetching ? (
+
+ Loading configuration...
+
+ ) : Object.keys(discountConfig).length > 0 ? (
+
+ ) : (
+
+
+
+ No provider discounts configured
+
+
+ Click "Add Provider Discount" to get started
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Accordion 2: Fee/Price Margin */}
+
+
+
+ Fee/Price Margin
+
+ Add fees or margins to LLM costs for internal billing and cost recovery
+
+
+
+
+
+
+
+
{isFetching ? (
Loading configuration...
- ) : Object.keys(discountConfig).length > 0 ? (
-
+ ) : Object.keys(marginConfig).length > 0 ? (
+
) : (
- No provider discounts configured
+ No provider margins configured
- Click "Add Provider Discount" to get started
+ Click "Add Provider Margin" to get started
)}
-
-
-
-
-
-
-
-
+
+
+
= ({
+
+
+ Add Provider Margin
+
+ }
+ open={isMarginModalVisible}
+ width={1000}
+ onCancel={handleMarginModalCancel}
+ footer={null}
+ className="top-8"
+ styles={{
+ body: { padding: "24px" },
+ header: { padding: "24px 24px 0 24px", border: "none" },
+ }}
+ >
+
+
+ Select a provider (or "Global" for all providers) and configure the margin. You can use percentage-based or fixed amount.
+
+
+
+
);
};
diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/index.ts b/ui/litellm-dashboard/src/components/CostTrackingSettings/index.ts
index 11adc414664..feba943154b 100644
--- a/ui/litellm-dashboard/src/components/CostTrackingSettings/index.ts
+++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/index.ts
@@ -1,8 +1,12 @@
export { default as CostTrackingSettings } from "./cost_tracking_settings";
export { default as ProviderDiscountTable } from "./provider_discount_table";
export { default as AddProviderForm } from "./add_provider_form";
+export { default as ProviderMarginTable } from "./provider_margin_table";
+export { default as AddMarginForm } from "./add_margin_form";
export { default as HowItWorks } from "./how_it_works";
-export type { CostTrackingSettingsProps, DiscountConfig, CostDiscountResponse } from "./types";
+export type { CostTrackingSettingsProps, DiscountConfig, CostDiscountResponse, MarginConfig, CostMarginResponse } from "./types";
export type { ProviderDisplayInfo } from "./provider_display_helpers";
export * from "./provider_display_helpers";
+export { useDiscountConfig } from "./use_discount_config";
+export { useMarginConfig } from "./use_margin_config";
diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/provider_margin_table.tsx b/ui/litellm-dashboard/src/components/CostTrackingSettings/provider_margin_table.tsx
new file mode 100644
index 00000000000..f75fefef3e1
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/provider_margin_table.tsx
@@ -0,0 +1,206 @@
+import React, { useState } from "react";
+import { TextInput, Icon, Text } from "@tremor/react";
+import { TrashIcon, PencilAltIcon, CheckIcon, XIcon } from "@heroicons/react/outline";
+import { SimpleTable } from "../common_components/simple_table";
+import { MarginConfig } from "./types";
+import { getProviderDisplayInfo, handleImageError } from "./provider_display_helpers";
+
+interface ProviderMarginTableProps {
+ marginConfig: MarginConfig;
+ onMarginChange: (provider: string, value: number | { percentage?: number; fixed_amount?: number }) => void;
+ onRemoveProvider: (provider: string, providerDisplayName: string) => void;
+}
+
+interface ProviderMarginRow {
+ provider: string;
+ margin: number | { percentage?: number; fixed_amount?: number };
+}
+
+const ProviderMarginTable: React.FC = ({
+ marginConfig,
+ onMarginChange,
+ onRemoveProvider,
+}) => {
+ const [editingProvider, setEditingProvider] = useState(null);
+ const [editPercentage, setEditPercentage] = useState("");
+ const [editFixedAmount, setEditFixedAmount] = useState("");
+
+ const handleStartEdit = (provider: string, currentMargin: number | { percentage?: number; fixed_amount?: number }) => {
+ setEditingProvider(provider);
+ if (typeof currentMargin === "number") {
+ // Simple percentage format
+ setEditPercentage((currentMargin * 100).toString());
+ setEditFixedAmount("");
+ } else {
+ // Complex format with percentage and/or fixed_amount
+ setEditPercentage(currentMargin.percentage ? (currentMargin.percentage * 100).toString() : "");
+ setEditFixedAmount(currentMargin.fixed_amount ? currentMargin.fixed_amount.toString() : "");
+ }
+ };
+
+ const handleSaveEdit = (provider: string) => {
+ const percentValue = editPercentage ? parseFloat(editPercentage) : undefined;
+ const fixedValue = editFixedAmount ? parseFloat(editFixedAmount) : undefined;
+
+ if (percentValue !== undefined && !isNaN(percentValue) && percentValue >= 0 && percentValue <= 1000) {
+ if (fixedValue !== undefined && !isNaN(fixedValue) && fixedValue >= 0) {
+ // Both percentage and fixed amount
+ onMarginChange(provider, { percentage: percentValue / 100, fixed_amount: fixedValue });
+ } else {
+ // Only percentage
+ onMarginChange(provider, percentValue / 100);
+ }
+ } else if (fixedValue !== undefined && !isNaN(fixedValue) && fixedValue >= 0) {
+ // Only fixed amount
+ onMarginChange(provider, { fixed_amount: fixedValue });
+ }
+ setEditingProvider(null);
+ setEditPercentage("");
+ setEditFixedAmount("");
+ };
+
+ const handleCancelEdit = () => {
+ setEditingProvider(null);
+ setEditPercentage("");
+ setEditFixedAmount("");
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent, provider: string) => {
+ if (e.key === 'Enter') {
+ handleSaveEdit(provider);
+ } else if (e.key === 'Escape') {
+ handleCancelEdit();
+ }
+ };
+
+ const formatMargin = (margin: number | { percentage?: number; fixed_amount?: number }): string => {
+ if (typeof margin === "number") {
+ return `${(margin * 100).toFixed(1)}%`;
+ }
+ const parts: string[] = [];
+ if (margin.percentage !== undefined) {
+ parts.push(`${(margin.percentage * 100).toFixed(1)}%`);
+ }
+ if (margin.fixed_amount !== undefined) {
+ parts.push(`$${margin.fixed_amount.toFixed(6)}`);
+ }
+ return parts.join(" + ") || "0%";
+ };
+
+ // Convert margin config to array and sort (global first, then alphabetically)
+ const data: ProviderMarginRow[] = Object.entries(marginConfig)
+ .map(([provider, margin]) => ({ provider, margin }))
+ .sort((a, b) => {
+ if (a.provider === "global") return -1;
+ if (b.provider === "global") return 1;
+ const displayA = getProviderDisplayInfo(a.provider).displayName;
+ const displayB = getProviderDisplayInfo(b.provider).displayName;
+ return displayA.localeCompare(displayB);
+ });
+
+ return (
+ {
+ if (row.provider === "global") {
+ return (
+
+ Global (All Providers)
+
+ );
+ }
+ const { displayName, logo } = getProviderDisplayInfo(row.provider);
+ return (
+
+ {logo && (
+

handleImageError(e, displayName)}
+ />
+ )}
+
{displayName}
+
+ );
+ },
+ },
+ {
+ header: "Margin",
+ cell: (row) => (
+
+ {editingProvider === row.provider ? (
+ <>
+
+
+ %
+ +
+ $
+
+
+
handleSaveEdit(row.provider)}
+ className="cursor-pointer text-green-600 hover:text-green-700"
+ />
+
+ >
+ ) : (
+ <>
+ {formatMargin(row.margin)}
+ handleStartEdit(row.provider, row.margin)}
+ className="cursor-pointer text-blue-600 hover:text-blue-700"
+ />
+ >
+ )}
+
+ ),
+ width: "350px",
+ },
+ {
+ header: "Actions",
+ cell: (row) => {
+ const displayName = row.provider === "global" ? "Global" : getProviderDisplayInfo(row.provider).displayName;
+ return (
+ onRemoveProvider(row.provider, displayName)}
+ className="cursor-pointer hover:text-red-600"
+ />
+ );
+ },
+ width: "80px",
+ },
+ ]}
+ getRowKey={(row) => row.provider}
+ emptyMessage="No provider margins configured"
+ />
+ );
+};
+
+export default ProviderMarginTable;
+
diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/types.ts b/ui/litellm-dashboard/src/components/CostTrackingSettings/types.ts
index 55d49ecffd9..1e79110dfb3 100644
--- a/ui/litellm-dashboard/src/components/CostTrackingSettings/types.ts
+++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/types.ts
@@ -12,3 +12,11 @@ export interface CostDiscountResponse {
values: DiscountConfig;
}
+export interface MarginConfig {
+ [provider: string]: number | { percentage?: number; fixed_amount?: number };
+}
+
+export interface CostMarginResponse {
+ values: MarginConfig;
+}
+
diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/use_discount_config.ts b/ui/litellm-dashboard/src/components/CostTrackingSettings/use_discount_config.ts
new file mode 100644
index 00000000000..0ed57aa8cc2
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/use_discount_config.ts
@@ -0,0 +1,151 @@
+import { useState, useCallback } from "react";
+import { getProxyBaseUrl } from "@/components/networking";
+import NotificationsManager from "../molecules/notifications_manager";
+import { DiscountConfig } from "./types";
+import { getProviderBackendValue } from "./provider_display_helpers";
+import { Providers } from "../provider_info_helpers";
+
+export interface UseDiscountConfigProps {
+ accessToken: string | null;
+}
+
+export interface UseDiscountConfigReturn {
+ discountConfig: DiscountConfig;
+ setDiscountConfig: React.Dispatch>;
+ fetchDiscountConfig: () => Promise;
+ saveDiscountConfig: (config: DiscountConfig) => Promise;
+ handleAddProvider: (selectedProvider: string | undefined, newDiscount: string) => Promise;
+ handleRemoveProvider: (provider: string) => Promise;
+ handleDiscountChange: (provider: string, value: string) => Promise;
+}
+
+export function useDiscountConfig({ accessToken }: UseDiscountConfigProps): UseDiscountConfigReturn {
+ const [discountConfig, setDiscountConfig] = useState({});
+
+ const fetchDiscountConfig = useCallback(async () => {
+ try {
+ const proxyBaseUrl = getProxyBaseUrl();
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/config/cost_discount_config`
+ : "/config/cost_discount_config";
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setDiscountConfig(data.values || {});
+ } else {
+ console.error("Failed to fetch discount config");
+ }
+ } catch (error) {
+ console.error("Error fetching discount config:", error);
+ NotificationsManager.fromBackend("Failed to fetch discount configuration");
+ }
+ }, [accessToken]);
+
+ const saveDiscountConfig = useCallback(async (config: DiscountConfig) => {
+ try {
+ const proxyBaseUrl = getProxyBaseUrl();
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/config/cost_discount_config`
+ : "/config/cost_discount_config";
+
+ const response = await fetch(url, {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(config),
+ });
+
+ if (response.ok) {
+ NotificationsManager.success("Discount configuration updated successfully");
+ await fetchDiscountConfig();
+ } else {
+ const errorData = await response.json();
+ const errorMessage = errorData.detail?.error || errorData.detail || "Failed to update settings";
+ NotificationsManager.fromBackend(errorMessage);
+ }
+ } catch (error) {
+ console.error("Error updating discount config:", error);
+ NotificationsManager.fromBackend("Failed to update discount configuration");
+ }
+ }, [accessToken, fetchDiscountConfig]);
+
+ const handleAddProvider = useCallback(async (
+ selectedProvider: string | undefined,
+ newDiscount: string
+ ): Promise => {
+ if (!selectedProvider || !newDiscount) {
+ NotificationsManager.fromBackend("Please select a provider and enter discount percentage");
+ return false;
+ }
+
+ const percentageValue = parseFloat(newDiscount);
+ if (isNaN(percentageValue) || percentageValue < 0 || percentageValue > 100) {
+ NotificationsManager.fromBackend("Discount must be between 0% and 100%");
+ return false;
+ }
+
+ const providerValue = getProviderBackendValue(selectedProvider);
+
+ if (!providerValue) {
+ NotificationsManager.fromBackend("Invalid provider selected");
+ return false;
+ }
+
+ if (discountConfig[providerValue]) {
+ NotificationsManager.fromBackend(
+ `Discount for ${Providers[selectedProvider as keyof typeof Providers]} already exists. Edit it in the table above.`
+ );
+ return false;
+ }
+
+ const discountValue = percentageValue / 100;
+ const updatedConfig = {
+ ...discountConfig,
+ [providerValue]: discountValue,
+ };
+
+ setDiscountConfig(updatedConfig);
+ await saveDiscountConfig(updatedConfig);
+ return true;
+ }, [discountConfig, saveDiscountConfig]);
+
+ const handleRemoveProvider = useCallback(async (provider: string) => {
+ const updatedConfig = { ...discountConfig };
+ delete updatedConfig[provider];
+ setDiscountConfig(updatedConfig);
+ await saveDiscountConfig(updatedConfig);
+ }, [discountConfig, saveDiscountConfig]);
+
+ const handleDiscountChange = useCallback(async (provider: string, value: string) => {
+ const discountValue = parseFloat(value);
+ if (!isNaN(discountValue) && discountValue >= 0 && discountValue <= 1) {
+ const updatedConfig = {
+ ...discountConfig,
+ [provider]: discountValue,
+ };
+ setDiscountConfig(updatedConfig);
+ await saveDiscountConfig(updatedConfig);
+ }
+ }, [discountConfig, saveDiscountConfig]);
+
+ return {
+ discountConfig,
+ setDiscountConfig,
+ fetchDiscountConfig,
+ saveDiscountConfig,
+ handleAddProvider,
+ handleRemoveProvider,
+ handleDiscountChange,
+ };
+}
+
diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/use_margin_config.ts b/ui/litellm-dashboard/src/components/CostTrackingSettings/use_margin_config.ts
new file mode 100644
index 00000000000..f443e1c121e
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/use_margin_config.ts
@@ -0,0 +1,176 @@
+import { useState, useCallback } from "react";
+import { getProxyBaseUrl } from "@/components/networking";
+import NotificationsManager from "../molecules/notifications_manager";
+import { MarginConfig } from "./types";
+import { getProviderBackendValue } from "./provider_display_helpers";
+import { Providers } from "../provider_info_helpers";
+
+export interface UseMarginConfigProps {
+ accessToken: string | null;
+}
+
+export interface UseMarginConfigReturn {
+ marginConfig: MarginConfig;
+ setMarginConfig: React.Dispatch>;
+ fetchMarginConfig: () => Promise;
+ saveMarginConfig: (config: MarginConfig) => Promise;
+ handleAddMargin: (params: AddMarginParams) => Promise;
+ handleRemoveMargin: (provider: string) => Promise;
+ handleMarginChange: (
+ provider: string,
+ value: number | { percentage?: number; fixed_amount?: number }
+ ) => Promise;
+}
+
+export interface AddMarginParams {
+ selectedProvider: string | undefined;
+ marginType: "percentage" | "fixed";
+ percentageValue: string;
+ fixedAmountValue: string;
+}
+
+export function useMarginConfig({ accessToken }: UseMarginConfigProps): UseMarginConfigReturn {
+ const [marginConfig, setMarginConfig] = useState({});
+
+ const fetchMarginConfig = useCallback(async () => {
+ try {
+ const proxyBaseUrl = getProxyBaseUrl();
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/config/cost_margin_config`
+ : "/config/cost_margin_config";
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setMarginConfig(data.values || {});
+ } else {
+ console.error("Failed to fetch margin config");
+ }
+ } catch (error) {
+ console.error("Error fetching margin config:", error);
+ NotificationsManager.fromBackend("Failed to fetch margin configuration");
+ }
+ }, [accessToken]);
+
+ const saveMarginConfig = useCallback(async (config: MarginConfig) => {
+ try {
+ const proxyBaseUrl = getProxyBaseUrl();
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/config/cost_margin_config`
+ : "/config/cost_margin_config";
+
+ const response = await fetch(url, {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(config),
+ });
+
+ if (response.ok) {
+ NotificationsManager.success("Margin configuration updated successfully");
+ await fetchMarginConfig();
+ } else {
+ const errorData = await response.json();
+ const errorMessage = errorData.detail?.error || errorData.detail || "Failed to update settings";
+ NotificationsManager.fromBackend(errorMessage);
+ }
+ } catch (error) {
+ console.error("Error updating margin config:", error);
+ NotificationsManager.fromBackend("Failed to update margin configuration");
+ }
+ }, [accessToken, fetchMarginConfig]);
+
+ const handleAddMargin = useCallback(async (params: AddMarginParams): Promise => {
+ const { selectedProvider, marginType, percentageValue, fixedAmountValue } = params;
+
+ if (!selectedProvider) {
+ NotificationsManager.fromBackend("Please select a provider");
+ return false;
+ }
+
+ let providerValue: string;
+ if (selectedProvider === "global") {
+ providerValue = "global";
+ } else {
+ const backendValue = getProviderBackendValue(selectedProvider);
+ if (!backendValue) {
+ NotificationsManager.fromBackend("Invalid provider selected");
+ return false;
+ }
+ providerValue = backendValue;
+ }
+
+ if (marginConfig[providerValue]) {
+ const displayName = providerValue === "global" ? "Global" : Providers[selectedProvider as keyof typeof Providers];
+ NotificationsManager.fromBackend(
+ `Margin for ${displayName} already exists. Edit it in the table above.`
+ );
+ return false;
+ }
+
+ let marginValue: number | { fixed_amount?: number };
+ if (marginType === "percentage") {
+ const percentValue = parseFloat(percentageValue);
+ if (isNaN(percentValue) || percentValue < 0 || percentValue > 1000) {
+ NotificationsManager.fromBackend("Percentage must be between 0% and 1000%");
+ return false;
+ }
+ marginValue = percentValue / 100;
+ } else {
+ const fixedValue = parseFloat(fixedAmountValue);
+ if (isNaN(fixedValue) || fixedValue < 0) {
+ NotificationsManager.fromBackend("Fixed amount must be non-negative");
+ return false;
+ }
+ marginValue = { fixed_amount: fixedValue };
+ }
+
+ const updatedConfig = {
+ ...marginConfig,
+ [providerValue]: marginValue,
+ };
+
+ setMarginConfig(updatedConfig);
+ await saveMarginConfig(updatedConfig);
+ return true;
+ }, [marginConfig, saveMarginConfig]);
+
+ const handleRemoveMargin = useCallback(async (provider: string) => {
+ const updatedConfig = { ...marginConfig };
+ delete updatedConfig[provider];
+ setMarginConfig(updatedConfig);
+ await saveMarginConfig(updatedConfig);
+ }, [marginConfig, saveMarginConfig]);
+
+ const handleMarginChange = useCallback(async (
+ provider: string,
+ value: number | { percentage?: number; fixed_amount?: number }
+ ) => {
+ const updatedConfig = {
+ ...marginConfig,
+ [provider]: value,
+ };
+ setMarginConfig(updatedConfig);
+ await saveMarginConfig(updatedConfig);
+ }, [marginConfig, saveMarginConfig]);
+
+ return {
+ marginConfig,
+ setMarginConfig,
+ fetchMarginConfig,
+ saveMarginConfig,
+ handleAddMargin,
+ handleRemoveMargin,
+ handleMarginChange,
+ };
+}
+
diff --git a/ui/litellm-dashboard/src/components/OldTeams.test.tsx b/ui/litellm-dashboard/src/components/OldTeams.test.tsx
index f3b4ec82d53..76fc26a8847 100644
--- a/ui/litellm-dashboard/src/components/OldTeams.test.tsx
+++ b/ui/litellm-dashboard/src/components/OldTeams.test.tsx
@@ -1,10 +1,13 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
+import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key";
import { fetchMCPAccessGroups, getGuardrailsList, teamCreateCall } from "./networking";
import OldTeams from "./OldTeams";
const mockTeamInfoView = vi.fn();
+const mockUseOrganizations = vi.fn();
vi.mock("./networking", () => ({
teamCreateCall: vi.fn(),
@@ -57,6 +60,25 @@ vi.mock("@/components/team/team_info", () => ({
},
}));
+vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
+ useOrganizations: () => mockUseOrganizations(),
+}));
+
+const createQueryClient = () => {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+};
+
+const renderWithQueryClient = (component: React.ReactElement) => {
+ const queryClient = createQueryClient();
+ return render({component});
+};
+
describe("OldTeams - handleCreate organization handling", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -64,6 +86,7 @@ describe("OldTeams - handleCreate organization handling", () => {
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue([]);
vi.mocked(fetchMCPAccessGroups).mockResolvedValue([]);
vi.mocked(getGuardrailsList).mockResolvedValue({ guardrails: [] });
+ mockUseOrganizations.mockReturnValue({ data: null });
});
it("should not include organization_id when it's an empty string", async () => {
@@ -274,7 +297,8 @@ describe("OldTeams - handleCreate organization handling", () => {
});
it("should clear the delete modal when the cancel button is clicked", async () => {
- render(
+ mockUseOrganizations.mockReturnValue({ data: [] });
+ renderWithQueryClient(
{
describe("OldTeams - empty state", () => {
beforeEach(() => {
vi.clearAllMocks();
+ mockUseOrganizations.mockReturnValue({ data: [] });
});
it("should display empty state message when teams array is empty", () => {
- render(
+ renderWithQueryClient(
{
});
it("should display empty state message when teams is null", () => {
- render(
+ renderWithQueryClient(
{
});
it("should not display empty state when teams array has items", () => {
- render(
+ renderWithQueryClient(
{
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue([]);
vi.mocked(fetchMCPAccessGroups).mockResolvedValue([]);
vi.mocked(getGuardrailsList).mockResolvedValue({ guardrails: [] });
+ mockUseOrganizations.mockReturnValue({ data: [] });
});
it("passes premiumUser flag to TeamInfoView", async () => {
- render(
+ renderWithQueryClient(
{
describe("OldTeams - Default Team Settings tab visibility", () => {
beforeEach(() => {
vi.clearAllMocks();
+ mockUseOrganizations.mockReturnValue({ data: [] });
});
it("should show Default Team Settings tab for Admin role", () => {
- render(
+ renderWithQueryClient(
{
});
it("should show Default Team Settings tab for proxy_admin role", () => {
- render(
+ renderWithQueryClient(
{
});
it("should not show Default Team Settings tab for proxy_admin_viewer role", () => {
- render(
+ renderWithQueryClient(
{
});
it("should not show Default Team Settings tab for Admin Viewer role", () => {
- render(
+ renderWithQueryClient(
{
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
+ mockUseOrganizations.mockReturnValue({ data: [] });
});
it("should not render all-proxy-models option in models select", async () => {
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
- render(
+ renderWithQueryClient(
{
expect(allProxyModelsOption).not.toBeInTheDocument();
});
});
+
+describe("OldTeams - organization alias display", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseOrganizations.mockReturnValue({ data: [] });
+ });
+
+ it("should display organization alias instead of organization id", () => {
+ const mockOrganizations = [
+ {
+ organization_id: "org-123",
+ organization_alias: "Test Organization",
+ budget_id: "budget-1",
+ metadata: {},
+ models: [],
+ spend: 0,
+ model_spend: {},
+ created_at: new Date().toISOString(),
+ created_by: "user-1",
+ updated_at: new Date().toISOString(),
+ updated_by: "user-1",
+ litellm_budget_table: null,
+ teams: null,
+ users: null,
+ members: null,
+ },
+ ];
+
+ mockUseOrganizations.mockReturnValue({ data: mockOrganizations });
+
+ renderWithQueryClient(
+ ,
+ );
+
+ expect(screen.getByText("Test Organization")).toBeInTheDocument();
+ expect(screen.queryByText("org-123")).not.toBeInTheDocument();
+ });
+
+ it("should display organization id when alias is not found", () => {
+ mockUseOrganizations.mockReturnValue({ data: [] });
+
+ renderWithQueryClient(
+ ,
+ );
+
+ expect(screen.getByText("org-unknown")).toBeInTheDocument();
+ });
+
+ it("should display N/A when organization_id is null", () => {
+ mockUseOrganizations.mockReturnValue({ data: [] });
+
+ renderWithQueryClient(
+ ,
+ );
+
+ expect(screen.getByText("N/A")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx
index 562d75c327a..10a38f0285c 100644
--- a/ui/litellm-dashboard/src/components/OldTeams.tsx
+++ b/ui/litellm-dashboard/src/components/OldTeams.tsx
@@ -1,3 +1,4 @@
+import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import AvailableTeamsPanel from "@/components/team/available_teams";
import TeamInfoView from "@/components/team/team_info";
import TeamSSOSettings from "@/components/TeamSSOSettings";
@@ -149,6 +150,18 @@ const getAdminOrganizations = (
return [];
};
+const getOrganizationAlias = (
+ organizationId: string | null | undefined,
+ organizations: Organization[] | null | undefined,
+): string => {
+ if (!organizationId || !organizations) {
+ return organizationId || "N/A";
+ }
+
+ const organization = organizations.find((org) => org.organization_id === organizationId);
+ return organization?.organization_alias || organizationId;
+};
+
// @deprecated
const Teams: React.FC = ({
teams,
@@ -161,6 +174,7 @@ const Teams: React.FC = ({
premiumUser = false,
}) => {
console.log(`organizations: ${JSON.stringify(organizations)}`);
+ const { data: organizationsData } = useOrganizations();
const [lastRefreshed, setLastRefreshed] = useState("");
const [currentOrg, setCurrentOrg] = useState(null);
const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null);
@@ -940,7 +954,9 @@ const Teams: React.FC = ({
- {team.organization_id}
+
+ {getOrganizationAlias(team.organization_id, organizationsData || organizations)}
+
{perTeamInfo &&
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
index 8f6bc411630..db268286007 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
@@ -268,16 +268,7 @@ const TopKeyView: React.FC = ({ topKeys, teams, showTags = fals
{/* Content */}
-
+
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx
index 9766983c36a..920955138b9 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx
@@ -80,8 +80,8 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
});
const [allTags, setAllTags] = useState([]);
- const { data: customers = [] } = useCustomers(accessToken, userRole);
- const { data: agentsResponse } = useAgents(accessToken, userRole);
+ const { data: customers = [] } = useCustomers();
+ const { data: agentsResponse } = useAgents();
const [modelViewType, setModelViewType] = useState<"groups" | "individual">("groups");
const [isCloudZeroModalOpen, setIsCloudZeroModalOpen] = useState(false);
const [isGlobalExportModalOpen, setIsGlobalExportModalOpen] = useState(false);
diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
new file mode 100644
index 00000000000..3f55b11769c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
@@ -0,0 +1,266 @@
+import { screen, waitFor } from "@testing-library/react";
+import { vi, it, expect, beforeEach, MockedFunction } from "vitest";
+import { renderWithProviders } from "../../../tests/test-utils";
+import { VirtualKeysTable } from "./VirtualKeysTable";
+import { KeyResponse, Team } from "../key_team_helpers/key_list";
+import { Organization } from "../networking";
+import { KeysResponse, useKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
+import { useFilterLogic } from "../key_team_helpers/filter_logic";
+
+// Mock network calls
+vi.mock("./networking", async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ userListCall: vi.fn().mockResolvedValue({
+ users: [
+ {
+ user_id: "user-1",
+ user_email: "user@example.com",
+ user_role: "user",
+ },
+ ],
+ }),
+ };
+});
+
+// Mock filter helpers
+vi.mock("./key_team_helpers/filter_helpers", () => ({
+ fetchAllKeyAliases: vi.fn().mockResolvedValue(["test-key-alias"]),
+ fetchAllTeams: vi.fn().mockResolvedValue([
+ {
+ team_id: "team-1",
+ team_alias: "Test Team",
+ },
+ ]),
+ fetchAllOrganizations: vi.fn().mockResolvedValue([
+ {
+ organization_id: "org-1",
+ organization_alias: "Test Organization",
+ },
+ ]),
+}));
+
+// Mock useKeys hook
+vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({
+ useKeys: vi.fn(),
+}));
+
+// Mock useFilterLogic hook
+vi.mock("../key_team_helpers/filter_logic", () => ({
+ useFilterLogic: vi.fn(),
+}));
+
+const mockKey: KeyResponse = {
+ token: "sk-1234567890abcdef",
+ token_id: "key-1",
+ key_name: "test-key",
+ key_alias: "Test Key Alias",
+ spend: 5.5,
+ max_budget: 100,
+ expires: "2024-12-31T23:59:59Z",
+ models: ["gpt-3.5-turbo", "gpt-4"],
+ aliases: {},
+ config: {},
+ user_id: "user-1",
+ team_id: "team-1",
+ max_parallel_requests: 10,
+ metadata: {},
+ tpm_limit: 1000,
+ rpm_limit: 100,
+ duration: "30d",
+ budget_duration: "1m",
+ budget_reset_at: "2024-12-01T00:00:00Z",
+ allowed_cache_controls: [],
+ allowed_routes: [],
+ permissions: {},
+ model_spend: { "gpt-3.5-turbo": 2.5, "gpt-4": 3.0 },
+ model_max_budget: { "gpt-3.5-turbo": 50, "gpt-4": 50 },
+ soft_budget_cooldown: false,
+ blocked: false,
+ litellm_budget_table: {},
+ organization_id: "org-1",
+ created_at: "2024-11-01T10:00:00Z",
+ updated_at: "2024-11-15T10:00:00Z",
+ team_spend: 5.5,
+ team_alias: "Test Team",
+ team_tpm_limit: 5000,
+ team_rpm_limit: 500,
+ team_max_budget: 500,
+ team_models: ["gpt-3.5-turbo", "gpt-4"],
+ team_blocked: false,
+ soft_budget: 50,
+ team_model_aliases: {},
+ team_member_spend: 0,
+ team_metadata: {},
+ end_user_id: "end-user-1",
+ end_user_tpm_limit: 100,
+ end_user_rpm_limit: 10,
+ end_user_max_budget: 10,
+ last_refreshed_at: Date.now(),
+ api_key: "sk-1234567890abcdef",
+ user_role: "user",
+ rpm_limit_per_model: {},
+ tpm_limit_per_model: {},
+ user_tpm_limit: 1000,
+ user_rpm_limit: 100,
+ user_email: "user@example.com",
+ user: {
+ user_email: "user@example.com",
+ user_id: "user-1",
+ },
+};
+
+const mockTeam: Team = {
+ team_id: "team-1",
+ team_alias: "Test Team",
+ models: ["gpt-3.5-turbo", "gpt-4"],
+ max_budget: 500,
+ budget_duration: "1m",
+ tpm_limit: 5000,
+ rpm_limit: 500,
+ organization_id: "org-1",
+ created_at: "2024-10-01T10:00:00Z",
+ keys: [],
+ members_with_roles: [],
+};
+
+const mockOrganization: Organization = {
+ organization_id: "org-1",
+ organization_alias: "Test Organization",
+ budget_id: "budget-1",
+ metadata: {},
+ models: ["gpt-3.5-turbo", "gpt-4"],
+ spend: 100,
+ model_spend: { "gpt-3.5-turbo": 50, "gpt-4": 50 },
+ created_at: "2024-10-01T10:00:00Z",
+ created_by: "user-1",
+ updated_at: "2024-11-01T10:00:00Z",
+ updated_by: "user-1",
+ litellm_budget_table: {},
+ teams: [],
+ users: [],
+ members: [],
+};
+
+// Mock hook implementations
+const mockUseKeys = useKeys as MockedFunction;
+const mockUseFilterLogic = useFilterLogic as MockedFunction;
+
+beforeEach(() => {
+ // Reset mocks before each test
+ vi.clearAllMocks();
+
+ // Setup default mock implementations
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [mockKey],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ refetch: vi.fn(),
+ } as any);
+
+ mockUseFilterLogic.mockReturnValue({
+ filters: {
+ "Team ID": "team-1",
+ "Organization ID": "org-1",
+ "Key Alias": "Test Key Alias",
+ "User ID": "user-1",
+ "User Email": "user@example.com",
+ "User Role": "user",
+ "Sort By": "created_at",
+ "Sort Order": "desc",
+ },
+ filteredKeys: [mockKey],
+ allKeyAliases: ["test-key-alias"],
+ allTeams: [mockTeam],
+ allOrganizations: [mockOrganization],
+ handleFilterChange: vi.fn(),
+ handleFilterReset: vi.fn(),
+ });
+});
+
+it("should render VirtualKeysTable component", () => {
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
+});
+
+it("should display key information correctly", async () => {
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
+ expect(screen.getByText("Test Team")).toBeInTheDocument();
+ expect(screen.getByText("5.5000")).toBeInTheDocument();
+ });
+});
+
+it("should display user email correctly", async () => {
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByText("user@example.com")).toBeInTheDocument();
+ });
+});
+
+it("should show skeleton loaders when isLoading is true", () => {
+ // Mock loading state
+ mockUseKeys.mockReturnValue({
+ data: null,
+ isPending: true,
+ refetch: vi.fn(),
+ } as any);
+
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ // Check that loading message is shown
+ expect(screen.getByText("🚅 Loading keys...")).toBeInTheDocument();
+
+ // Check that actual key data is not shown
+ expect(screen.queryByText("Test Key Alias")).not.toBeInTheDocument();
+ expect(screen.queryByText("Test Team")).not.toBeInTheDocument();
+});
diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
similarity index 71%
rename from ui/litellm-dashboard/src/components/all_keys_table.tsx
rename to ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
index a915fe06179..b95d675979c 100644
--- a/ui/litellm-dashboard/src/components/all_keys_table.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
@@ -1,130 +1,55 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { ColumnDef } from "@tanstack/react-table";
-import { Select, SelectItem } from "@tremor/react";
-import { Button } from "@tremor/react";
-import KeyInfoView from "./templates/key_info_view";
-import { Tooltip } from "antd";
-import { Team, KeyResponse } from "./key_team_helpers/key_list";
-import FilterComponent from "./molecules/filter";
-import { FilterOption } from "./molecules/filter";
-import { Organization, userListCall } from "./networking";
-import { useFilterLogic } from "./key_team_helpers/filter_logic";
-import { Setter } from "@/types";
-import { updateExistingKeys } from "@/utils/dataUtils";
-import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table";
-import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react";
-import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
-import { Badge, Text } from "@tremor/react";
-import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
+import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
import { formatNumberWithCommas } from "@/utils/dataUtils";
+import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ PaginationState,
+ SortingState,
+ useReactTable,
+} from "@tanstack/react-table";
+import {
+ Badge,
+ Button,
+ Icon,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeaderCell,
+ TableRow,
+ Text,
+} from "@tremor/react";
+import { Skeleton, Tooltip } from "antd";
+import React, { useEffect, useState } from "react";
+import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
+import { useFilterLogic } from "../key_team_helpers/filter_logic";
+import { KeyResponse, Team } from "../key_team_helpers/key_list";
+import FilterComponent, { FilterOption } from "../molecules/filter";
+import { Organization } from "../networking";
+import KeyInfoView from "../templates/key_info_view";
-interface AllKeysTableProps {
- keys: KeyResponse[];
- setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void;
- isLoading?: boolean;
- pagination: {
- currentPage: number;
- totalPages: number;
- totalCount: number;
- };
- onPageChange: (page: number) => void;
- pageSize?: number;
+interface VirtualKeysTableProps {
teams: Team[] | null;
- selectedTeam: Team | null;
- setSelectedTeam: (team: Team | null) => void;
- selectedKeyAlias: string | null;
- setSelectedKeyAlias: Setter;
- accessToken: string | null;
- userID: string | null;
- userRole: string | null;
organizations: Organization[] | null;
- setCurrentOrg: React.Dispatch>;
- refresh?: () => void;
onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void;
currentSort?: {
sortBy: string;
sortOrder: "asc" | "desc";
};
- premiumUser: boolean;
- setAccessToken?: (token: string) => void;
}
-// Define columns similar to our logs table
-
-interface UserResponse {
- user_id: string;
- user_email: string;
- user_role: string;
-}
-
-const TeamFilter = ({
- teams,
- selectedTeam,
- setSelectedTeam,
-}: {
- teams: Team[] | null;
- selectedTeam: Team | null;
- setSelectedTeam: (team: Team | null) => void;
-}) => {
- const handleTeamChange = (value: string) => {
- const team = teams?.find((t) => t.team_id === value);
- setSelectedTeam(team || null);
- };
-
- return (
-
-
- Where Team is
-
-
-
- );
-};
-
/**
- * AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
+ * VirtualKeysTable – a new table for keys that mimics the table styling used in view_logs.
* The team selector and filtering have been removed so that all keys are shown.
*/
-export function AllKeysTable({
- keys,
- setKeys,
- isLoading = false,
- pagination,
- onPageChange,
- pageSize = 50,
- teams,
- selectedTeam,
- setSelectedTeam,
- selectedKeyAlias,
- setSelectedKeyAlias,
- accessToken,
- userID,
- userRole,
- organizations,
- setCurrentOrg,
- refresh,
- onSortChange,
- currentSort,
- premiumUser,
- setAccessToken,
-}: AllKeysTableProps) {
- const [selectedKeyId, setSelectedKeyId] = useState(null);
- const [userList, setUserList] = useState([]);
+export function VirtualKeysTable({ teams, organizations, onSortChange, currentSort }: VirtualKeysTableProps) {
+ const [selectedKey, setSelectedKey] = useState(null);
const [sorting, setSorting] = React.useState(() => {
if (currentSort) {
return [
@@ -141,34 +66,33 @@ export function AllKeysTable({
},
];
});
+ const [tablePagination, setTablePagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 100,
+ });
+
+ const {
+ data: keys,
+ isPending: isLoading,
+ refetch,
+ } = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize);
+ const totalCount = keys?.total_count || 0;
const [expandedAccordions, setExpandedAccordions] = useState>({});
// Use the filter logic hook
const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } =
useFilterLogic({
- keys,
+ keys: keys?.keys || [],
teams,
organizations,
- accessToken,
});
- useEffect(() => {
- if (accessToken) {
- const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null);
- const fetchUserList = async () => {
- const userListData = await userListCall(accessToken, user_IDs, 1, 100);
- setUserList(userListData.users);
- };
- fetchUserList();
- }
- }, [accessToken, keys]);
-
// Add a useEffect to call refresh when a key is created
useEffect(() => {
- if (refresh) {
+ if (refetch) {
const handleStorageChange = () => {
- refresh();
+ refetch();
};
// Listen for storage events that might indicate a key was created
@@ -178,12 +102,13 @@ export function AllKeysTable({
window.removeEventListener("storage", handleStorageChange);
};
}
- }, [refresh]);
+ }, [refetch]);
const columns: ColumnDef[] = [
{
id: "expander",
header: () => null,
+ size: 40,
cell: ({ row }) =>
row.getCanExpand() ? (