diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.test.tsx
new file mode 100644
index 0000000000..bbfc5fb5d6
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.test.tsx
@@ -0,0 +1,239 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import SpendByProvider from "./SpendByProvider";
+
+vi.mock("../../../shared/chart_loader", () => ({
+ ChartLoader: ({ isDateChanging }: { isDateChanging: boolean }) => (
+
+ {isDateChanging ? "Processing date selection..." : "Loading chart data..."}
+
+ ),
+}));
+
+vi.mock("../../../molecules/models/ProviderLogo", () => ({
+ ProviderLogo: ({ provider }: { provider: string }) => (
+ {provider}
+ ),
+}));
+
+describe("SpendByProvider", () => {
+ const mockProviderSpend = [
+ {
+ provider: "openai",
+ spend: 150.5,
+ requests: 100,
+ successful_requests: 95,
+ failed_requests: 5,
+ tokens: 50000,
+ },
+ {
+ provider: "anthropic",
+ spend: 200.75,
+ requests: 120,
+ successful_requests: 115,
+ failed_requests: 5,
+ tokens: 75000,
+ },
+ {
+ provider: "unknown",
+ spend: 0,
+ requests: 10,
+ successful_requests: 0,
+ failed_requests: 10,
+ tokens: 0,
+ },
+ {
+ provider: "google",
+ spend: 0,
+ requests: 0,
+ successful_requests: 0,
+ failed_requests: 0,
+ tokens: 0,
+ },
+ ];
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should render", () => {
+ render();
+ expect(screen.getByText("Spend by Provider")).toBeInTheDocument();
+ });
+
+ it("should display the title", () => {
+ render();
+ expect(screen.getByText("Spend by Provider")).toBeInTheDocument();
+ });
+
+ it("should display Show Zero Spend toggle", () => {
+ render();
+ expect(screen.getByText("Show Zero Spend")).toBeInTheDocument();
+ expect(screen.getAllByRole("switch")[0]).toBeInTheDocument();
+ });
+
+ it("should display Show Unknown toggle", () => {
+ render();
+ expect(screen.getByText("Show Unknown")).toBeInTheDocument();
+ expect(screen.getAllByRole("switch")[1]).toBeInTheDocument();
+ });
+
+ it("should display table headers", () => {
+ render();
+ expect(screen.getByText("Provider")).toBeInTheDocument();
+ expect(screen.getByText("Spend")).toBeInTheDocument();
+ expect(screen.getByText("Successful")).toBeInTheDocument();
+ expect(screen.getByText("Failed")).toBeInTheDocument();
+ expect(screen.getByText("Tokens")).toBeInTheDocument();
+ });
+
+ it("should display provider data in table", () => {
+ render();
+ expect(screen.getAllByText("openai").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("anthropic").length).toBeGreaterThan(0);
+ expect(screen.getByText("$150.50")).toBeInTheDocument();
+ expect(screen.getByText("$200.75")).toBeInTheDocument();
+ });
+
+ it("should display formatted spend values with two decimal places", () => {
+ render();
+ expect(screen.getByText("$150.50")).toBeInTheDocument();
+ expect(screen.getByText("$200.75")).toBeInTheDocument();
+ });
+
+ it("should display successful requests with locale formatting", () => {
+ render();
+ expect(screen.getByText("95")).toBeInTheDocument();
+ expect(screen.getByText("115")).toBeInTheDocument();
+ });
+
+ it("should display failed requests with locale formatting", () => {
+ render();
+ expect(screen.getAllByText("5").length).toBeGreaterThan(0);
+ });
+
+ it("should display tokens with locale formatting", () => {
+ render();
+ expect(screen.getByText("50,000")).toBeInTheDocument();
+ expect(screen.getByText("75,000")).toBeInTheDocument();
+ });
+
+ it("should display provider logos", () => {
+ render();
+ expect(screen.getByTestId("provider-logo-openai")).toBeInTheDocument();
+ expect(screen.getByTestId("provider-logo-anthropic")).toBeInTheDocument();
+ });
+
+ it("should filter out providers with zero spend by default", () => {
+ render();
+ expect(screen.getAllByText("openai").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("anthropic").length).toBeGreaterThan(0);
+ expect(screen.queryByText("google")).not.toBeInTheDocument();
+ });
+
+ it("should filter out unknown provider by default", () => {
+ render();
+ expect(screen.queryByText("unknown")).not.toBeInTheDocument();
+ });
+
+ it("should display ChartLoader when loading is true", () => {
+ render();
+ expect(screen.getByTestId("chart-loader")).toBeInTheDocument();
+ expect(screen.getByText("Loading chart data...")).toBeInTheDocument();
+ });
+
+ it("should display ChartLoader with date changing message when isDateChanging is true", () => {
+ render();
+ expect(screen.getByTestId("chart-loader")).toBeInTheDocument();
+ expect(screen.getByText("Processing date selection...")).toBeInTheDocument();
+ });
+
+ it("should not display table when loading is true", () => {
+ render();
+ expect(screen.queryByText("Provider")).not.toBeInTheDocument();
+ });
+
+ it("should handle empty provider spend array", () => {
+ render();
+ expect(screen.getByText("Provider")).toBeInTheDocument();
+ expect(screen.getByText("Spend")).toBeInTheDocument();
+ });
+
+ it("should handle provider with null provider name", () => {
+ const providerSpendWithNull = [
+ {
+ provider: null as unknown as string,
+ spend: 100,
+ requests: 50,
+ successful_requests: 45,
+ failed_requests: 5,
+ tokens: 25000,
+ },
+ ];
+ render();
+ expect(screen.getByText("$100.00")).toBeInTheDocument();
+ });
+
+ it("should handle provider with empty string provider name", () => {
+ const providerSpendWithEmpty = [
+ {
+ provider: "",
+ spend: 100,
+ requests: 50,
+ successful_requests: 45,
+ failed_requests: 5,
+ tokens: 25000,
+ },
+ ];
+ render();
+ expect(screen.getByText("$100.00")).toBeInTheDocument();
+ });
+
+ it("should display large token numbers with comma formatting", () => {
+ const providerSpendWithLargeTokens = [
+ {
+ provider: "openai",
+ spend: 1000,
+ requests: 1000,
+ successful_requests: 950,
+ failed_requests: 50,
+ tokens: 1234567,
+ },
+ ];
+ render();
+ expect(screen.getByText("1,234,567")).toBeInTheDocument();
+ });
+
+ it("should filter data correctly when both toggles are off", () => {
+ render();
+ expect(screen.getAllByText("openai").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("anthropic").length).toBeGreaterThan(0);
+ expect(screen.queryByText("google")).not.toBeInTheDocument();
+ expect(screen.queryByText("unknown")).not.toBeInTheDocument();
+ });
+
+ it("should include all providers with spend greater than zero by default", () => {
+ const providerSpendWithMixed = [
+ {
+ provider: "provider1",
+ spend: 0.01,
+ requests: 10,
+ successful_requests: 10,
+ failed_requests: 0,
+ tokens: 1000,
+ },
+ {
+ provider: "provider2",
+ spend: 0,
+ requests: 0,
+ successful_requests: 0,
+ failed_requests: 0,
+ tokens: 0,
+ },
+ ];
+ render();
+ expect(screen.getAllByText("provider1").length).toBeGreaterThan(0);
+ expect(screen.queryByText("provider2")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.tsx
new file mode 100644
index 0000000000..4bbcc48dc4
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.tsx
@@ -0,0 +1,131 @@
+import { formatNumberWithCommas } from "@/utils/dataUtils";
+import { InfoCircleOutlined } from "@ant-design/icons";
+import {
+ Card,
+ Col,
+ DonutChart,
+ Grid,
+ Switch,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeaderCell,
+ TableRow,
+ Title,
+} from "@tremor/react";
+import { Tooltip } from "antd";
+import React, { useState } from "react";
+import { ProviderLogo } from "../../../molecules/models/ProviderLogo";
+import { ChartLoader } from "../../../shared/chart_loader";
+
+interface ProviderSpendData {
+ provider: string;
+ spend: number;
+ requests: number;
+ successful_requests: number;
+ failed_requests: number;
+ tokens: number;
+}
+
+interface SpendByProviderProps {
+ loading: boolean;
+ isDateChanging: boolean;
+ providerSpend: ProviderSpendData[];
+}
+
+const SpendByProvider: React.FC = ({ loading, isDateChanging, providerSpend }) => {
+ const [includeZeroSpend, setIncludeZeroSpend] = useState(false);
+ const [includeUnknown, setIncludeUnknown] = useState(false);
+
+ const filteredProviderSpend = providerSpend.filter((provider) => {
+ const isUnknown = provider.provider?.toLowerCase() === "unknown";
+
+ // If includeUnknown is true, always include unknown provider
+ if (isUnknown) {
+ return includeUnknown;
+ }
+
+ // If includeZeroSpend is true, include all providers (including those with 0 spend)
+ // Otherwise, only include providers with spend > 0
+ if (includeZeroSpend) {
+ return true;
+ }
+
+ return provider.spend > 0;
+ });
+
+ return (
+
+
+
Spend by Provider
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+
+ `$${formatNumberWithCommas(value, 2)}`}
+ colors={["cyan"]}
+ />
+
+
+
+
+
+ Provider
+ Spend
+ Successful
+ Failed
+ Tokens
+
+
+
+ {filteredProviderSpend.map((provider) => (
+
+
+
+ {provider.provider &&
}
+
{provider.provider}
+
+
+ ${formatNumberWithCommas(provider.spend, 2)}
+
+ {provider.successful_requests.toLocaleString()}
+
+
+ {provider.failed_requests.toLocaleString()}
+
+ {provider.tokens.toLocaleString()}
+
+ ))}
+
+
+
+
+ )}
+
+ );
+};
+
+export default SpendByProvider;
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx
index 69a1bb4e30..48ff713db1 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx
@@ -1,20 +1,21 @@
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
import { useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../tests/test-utils";
import type { Organization } from "../../networking";
import * as networking from "../../networking";
-import NewUsagePage from "./UsagePageView";
+import UsagePage from "./UsagePageView";
// Polyfill ResizeObserver for test environment
beforeAll(() => {
if (typeof window !== "undefined" && !window.ResizeObserver) {
window.ResizeObserver = class ResizeObserver {
- observe() {}
- unobserve() {}
- disconnect() {}
+ observe() { }
+ unobserve() { }
+ disconnect() { }
} as any;
}
});
@@ -45,6 +46,43 @@ vi.mock("./EntityUsage/EntityUsage", () => ({
EntityList: [],
}));
+vi.mock("./EntityUsage/SpendByProvider", () => ({
+ default: () => Spend By Provider
,
+}));
+
+vi.mock("./EndpointUsage/EndpointUsage", () => ({
+ default: () => Endpoint Usage
,
+}));
+
+vi.mock("./UsageViewSelect/UsageViewSelect", () => ({
+ UsageViewSelect: ({ value, onChange }: any) => {
+ const React = require("react");
+ return React.createElement(
+ "select",
+ {
+ value,
+ onChange: (e: any) => onChange?.(e.target.value),
+ role: "combobox",
+ "data-testid": "usage-view-select",
+ },
+ React.createElement("option", { value: "global" }, "Global Usage"),
+ React.createElement("option", { value: "team" }, "Team Usage"),
+ React.createElement("option", { value: "organization" }, "Organization Usage"),
+ React.createElement("option", { value: "customer" }, "Customer Usage"),
+ React.createElement("option", { value: "tag" }, "Tag Usage"),
+ React.createElement("option", { value: "agent" }, "Agent Usage"),
+ React.createElement("option", { value: "user-agent-activity" }, "User Agent Activity"),
+ );
+ },
+}));
+
+vi.mock("../../shared/advanced_date_picker", () => ({
+ default: ({ value, onValueChange }: any) => {
+ const React = require("react");
+ return React.createElement("div", { "data-testid": "advanced-date-picker" }, "Date Picker");
+ },
+}));
+
vi.mock("../../user_agent_activity", () => ({
default: () => User Agent Activity
,
}));
@@ -70,6 +108,10 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: vi.fn(),
}));
+vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({
+ useCurrentUser: vi.fn(),
+}));
+
vi.mock("antd", async (importOriginal) => {
const React = await import("react");
const actual = await importOriginal();
@@ -133,12 +175,40 @@ vi.mock("antd", async (importOriginal) => {
}
(Table as any).displayName = "Table";
+ function Segmented(props: any) {
+ const { value, onChange, options, ...rest } = props;
+ return React.createElement(
+ "div",
+ { ...rest, "data-testid": "antd-segmented" },
+ options?.map((opt: any) =>
+ React.createElement(
+ "button",
+ {
+ key: opt.value,
+ onClick: () => onChange?.(opt.value),
+ "data-selected": value === opt.value,
+ },
+ opt.label,
+ ),
+ ),
+ );
+ }
+ (Segmented as any).displayName = "AntdSegmented";
+
+ function Tooltip(props: any) {
+ const { title, children, ...rest } = props;
+ return React.createElement("div", { ...rest, "data-testid": "antd-tooltip", title }, children);
+ }
+ (Tooltip as any).displayName = "AntdTooltip";
+
return {
...actual,
Select,
Alert,
Badge,
Table,
+ Segmented,
+ Tooltip,
};
});
@@ -160,15 +230,97 @@ vi.mock("@ant-design/icons", async () => {
BarChartOutlined: Icon,
ClockCircleOutlined: Icon,
CalendarOutlined: Icon,
+ InfoCircleOutlined: Icon,
};
});
-describe("NewUsage", () => {
+// Mock Tremor components
+vi.mock("@tremor/react", async () => {
+ const React = await import("react");
+ const actual = await import("@tremor/react");
+
+ function TabGroup({ children }: any) {
+ return React.createElement("div", { "data-testid": "tremor-tab-group" }, children);
+ }
+
+ function TabList({ children }: any) {
+ return React.createElement("div", { "data-testid": "tremor-tab-list" }, children);
+ }
+
+ function Tab({ children, ...props }: any) {
+ return React.createElement("button", { ...props, "data-testid": "tremor-tab" }, children);
+ }
+
+ function TabPanels({ children }: any) {
+ return React.createElement("div", { "data-testid": "tremor-tab-panels" }, children);
+ }
+
+ function TabPanel({ children }: any) {
+ return React.createElement("div", { "data-testid": "tremor-tab-panel" }, children);
+ }
+
+ function Card({ children, ...props }: any) {
+ return React.createElement("div", { ...props, "data-testid": "tremor-card" }, children);
+ }
+
+ function Grid({ children, numItems, ...props }: any) {
+ return React.createElement("div", { ...props, "data-testid": "tremor-grid" }, children);
+ }
+
+ function Col({ children, numColSpan, ...props }: any) {
+ return React.createElement("div", { ...props, "data-testid": "tremor-col" }, children);
+ }
+
+ function Title({ children, ...props }: any) {
+ return React.createElement("h2", { ...props, "data-testid": "tremor-title" }, children);
+ }
+
+ function Text({ children, ...props }: any) {
+ return React.createElement("p", { ...props, "data-testid": "tremor-text" }, children);
+ }
+
+ function BarChart({ data, valueFormatter, yAxisWidth, showLegend, customTooltip, ...props }: any) {
+ return React.createElement("div", { ...props, "data-testid": "tremor-bar-chart" }, "Bar Chart");
+ }
+
+ function DonutChart({ data, ...props }: any) {
+ return React.createElement("div", { ...props, "data-testid": "tremor-donut-chart" }, "Donut Chart");
+ }
+
+ function Button({ children, icon, onClick, ...props }: any) {
+ return React.createElement(
+ "button",
+ { ...props, onClick, "data-testid": "tremor-button" },
+ icon && React.createElement("span", { "data-testid": "tremor-button-icon" }),
+ children,
+ );
+ }
+
+ return {
+ ...actual,
+ TabGroup,
+ TabList,
+ Tab,
+ TabPanels,
+ TabPanel,
+ Card,
+ Grid,
+ Col,
+ Title,
+ Text,
+ BarChart,
+ DonutChart,
+ Button,
+ };
+});
+
+describe("UsagePage", () => {
const mockUserDailyActivityAggregatedCall = vi.mocked(networking.userDailyActivityAggregatedCall);
const mockTagListCall = vi.mocked(networking.tagListCall);
const mockUseCustomers = vi.mocked(useCustomers);
const mockUseAgents = vi.mocked(useAgents);
const mockUseAuthorized = vi.mocked(useAuthorized);
+ const mockUseCurrentUser = vi.mocked(useCurrentUser);
const mockSpendData = {
results: [
@@ -308,9 +460,6 @@ describe("NewUsage", () => {
];
const defaultProps = {
- accessToken: "test-token",
- userRole: "Admin",
- userID: "user-123",
teams: [
{
team_id: "team-1",
@@ -330,20 +479,27 @@ describe("NewUsage", () => {
},
],
organizations: [],
- premiumUser: true,
};
beforeEach(() => {
mockUseAuthorized.mockReturnValue({
token: "mock-token",
- accessToken: defaultProps.accessToken,
- userId: defaultProps.userID,
+ accessToken: "test-token",
+ userId: "user-123",
userEmail: "test@example.com",
- userRole: defaultProps.userRole,
- premiumUser: defaultProps.premiumUser,
+ userRole: "Admin",
+ premiumUser: true,
disabledPersonalKeyCreation: false,
showSSOBanner: false,
});
+ mockUseCurrentUser.mockReturnValue({
+ data: {
+ user_id: "user-123",
+ max_budget: null,
+ },
+ isLoading: false,
+ error: null,
+ } as any);
mockUserDailyActivityAggregatedCall.mockClear();
mockTagListCall.mockClear();
mockUserDailyActivityAggregatedCall.mockResolvedValue(mockSpendData);
@@ -361,7 +517,7 @@ describe("NewUsage", () => {
});
it("should render and fetch usage data on mount", async () => {
- renderWithProviders();
+ renderWithProviders();
// Wait for data to be fetched
await waitFor(() => {
@@ -380,7 +536,7 @@ describe("NewUsage", () => {
});
it("should display usage metrics and charts", async () => {
- renderWithProviders();
+ renderWithProviders();
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
@@ -396,13 +552,13 @@ describe("NewUsage", () => {
const totalTokensElements = screen.getAllByText("Total Tokens");
expect(totalTokensElements.length).toBeGreaterThan(0);
- // Check for chart titles
+ // Check for chart titles (these are in the Cost tab)
expect(screen.getByText("Daily Spend")).toBeInTheDocument();
expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument();
});
it("should switch between usage views correctly", async () => {
- renderWithProviders();
+ renderWithProviders();
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
@@ -412,7 +568,7 @@ describe("NewUsage", () => {
expect(screen.getByText("Daily Spend")).toBeInTheDocument();
// Switch to Team Usage view
- const usageSelect = screen.getByRole("combobox");
+ const usageSelect = screen.getByTestId("usage-view-select");
act(() => {
fireEvent.change(usageSelect, { target: { value: "team" } });
});
@@ -436,13 +592,13 @@ describe("NewUsage", () => {
});
it("should show organization usage banner and view for admins", async () => {
- renderWithProviders();
+ renderWithProviders();
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
- const usageSelect = screen.getByRole("combobox");
+ const usageSelect = screen.getByTestId("usage-view-select");
act(() => {
fireEvent.change(usageSelect, { target: { value: "organization" } });
});
@@ -461,13 +617,13 @@ describe("NewUsage", () => {
error: null,
} as any);
- renderWithProviders();
+ renderWithProviders();
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
- const usageSelect = screen.getByRole("combobox");
+ const usageSelect = screen.getByTestId("usage-view-select");
act(() => {
fireEvent.change(usageSelect, { target: { value: "customer" } });
});
@@ -485,13 +641,13 @@ describe("NewUsage", () => {
error: null,
} as any);
- renderWithProviders();
+ renderWithProviders();
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
- const usageSelect = screen.getByRole("combobox");
+ const usageSelect = screen.getByTestId("usage-view-select");
act(() => {
fireEvent.change(usageSelect, { target: { value: "agent" } });
});
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx
index 88385248b6..688ee73767 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx
@@ -6,28 +6,22 @@
* Works at 1m+ spend logs, by querying an aggregate table instead.
*/
+import { InfoCircleOutlined } from "@ant-design/icons";
import {
BarChart,
Card,
Col,
DateRangePickerValue,
- DonutChart,
Grid,
Tab,
TabGroup,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeaderCell,
- TableRow,
TabList,
TabPanel,
TabPanels,
Text,
- Title,
+ Title
} from "@tremor/react";
-import { Alert, Segmented } from "antd";
+import { Alert, Segmented, Tooltip } from "antd";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
@@ -42,7 +36,6 @@ import CloudZeroExportModal from "../../cloudzero_export_modal";
import EntityUsageExportModal from "../../EntityUsageExport";
import { Team } from "../../key_team_helpers/key_list";
import { Organization, tagListCall, userDailyActivityAggregatedCall, userDailyActivityCall } from "../../networking";
-import { getProviderLogoAndName } from "../../provider_info_helpers";
import AdvancedDatePicker from "../../shared/advanced_date_picker";
import { ChartLoader } from "../../shared/chart_loader";
import { Tag } from "../../tag_management/types";
@@ -52,6 +45,7 @@ import { DailyData, KeyMetricWithMetadata, MetricWithMetadata } from "../types";
import { valueFormatterSpend } from "../utils/value_formatters";
import EndpointUsage from "./EndpointUsage/EndpointUsage";
import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage";
+import SpendByProvider from "./EntityUsage/SpendByProvider";
import TopKeyView from "./EntityUsage/TopKeyView";
import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect";
@@ -509,7 +503,12 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
- Failed Requests
+
+
Failed Requests
+
+
+
+
{userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0}
@@ -669,79 +668,11 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
{/* Spend by Provider */}
-
-
-
Spend by Provider
-
- {loading ? (
-
- ) : (
-
-
- `$${formatNumberWithCommas(value, 2)}`}
- colors={["cyan"]}
- />
-
-
-
-
-
- Provider
- Spend
- Successful
- Failed
- Tokens
-
-
-
- {getProviderSpend()
- .filter((provider) => provider.spend > 0)
- .map((provider) => (
-
-
-
- {provider.provider && (
-
.logo})
{
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement;
- if (parent) {
- const fallbackDiv = document.createElement("div");
- fallbackDiv.className =
- "w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs";
- fallbackDiv.textContent = provider.provider?.charAt(0) || "-";
- parent.replaceChild(fallbackDiv, target);
- }
- }}
- />
- )}
-
{provider.provider}
-
-
- ${formatNumberWithCommas(provider.spend, 2)}
-
- {provider.successful_requests.toLocaleString()}
-
-
- {provider.failed_requests.toLocaleString()}
-
- {provider.tokens.toLocaleString()}
-
- ))}
-
-
-
-
- )}
-
+
{/* Usage Metrics */}