From f64f94971569d46eb02656475408af82f83ecd9a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Feb 2026 16:22:07 -0800 Subject: [PATCH] Spend by provider refactor --- .../EntityUsage/SpendByProvider.test.tsx | 239 ++++++++++++++++++ .../EntityUsage/SpendByProvider.tsx | 131 ++++++++++ .../components/UsagePageView.test.tsx | 204 +++++++++++++-- .../UsagePage/components/UsagePageView.tsx | 99 ++------ 4 files changed, 565 insertions(+), 108 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.test.tsx create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/SpendByProvider.tsx 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 && ( - {`${provider.provider} { - 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 */}