Merge pull request #20539 from BerriAI/litellm_usage_failed_req

[Refactor] UI - Usage Page: Spend By Provider
This commit is contained in:
yuneng-jiang
2026-02-05 16:39:55 -08:00
committed by GitHub
4 changed files with 565 additions and 108 deletions
@@ -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 }) => (
<div data-testid="chart-loader">
{isDateChanging ? "Processing date selection..." : "Loading chart data..."}
</div>
),
}));
vi.mock("../../../molecules/models/ProviderLogo", () => ({
ProviderLogo: ({ provider }: { provider: string }) => (
<div data-testid={`provider-logo-${provider}`}>{provider}</div>
),
}));
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
expect(screen.getByText("Spend by Provider")).toBeInTheDocument();
});
it("should display the title", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
expect(screen.getByText("Spend by Provider")).toBeInTheDocument();
});
it("should display Show Zero Spend toggle", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
expect(screen.getByText("Show Zero Spend")).toBeInTheDocument();
expect(screen.getAllByRole("switch")[0]).toBeInTheDocument();
});
it("should display Show Unknown toggle", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
expect(screen.getByText("Show Unknown")).toBeInTheDocument();
expect(screen.getAllByRole("switch")[1]).toBeInTheDocument();
});
it("should display table headers", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.getByText("$150.50")).toBeInTheDocument();
expect(screen.getByText("$200.75")).toBeInTheDocument();
});
it("should display successful requests with locale formatting", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.getByText("95")).toBeInTheDocument();
expect(screen.getByText("115")).toBeInTheDocument();
});
it("should display failed requests with locale formatting", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.getAllByText("5").length).toBeGreaterThan(0);
});
it("should display tokens with locale formatting", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.getByText("50,000")).toBeInTheDocument();
expect(screen.getByText("75,000")).toBeInTheDocument();
});
it("should display provider logos", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.queryByText("unknown")).not.toBeInTheDocument();
});
it("should display ChartLoader when loading is true", () => {
render(<SpendByProvider loading={true} isDateChanging={false} providerSpend={mockProviderSpend} />);
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(<SpendByProvider loading={true} isDateChanging={true} providerSpend={mockProviderSpend} />);
expect(screen.getByTestId("chart-loader")).toBeInTheDocument();
expect(screen.getByText("Processing date selection...")).toBeInTheDocument();
});
it("should not display table when loading is true", () => {
render(<SpendByProvider loading={true} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.queryByText("Provider")).not.toBeInTheDocument();
});
it("should handle empty provider spend array", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithNull} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithEmpty} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithLargeTokens} />);
expect(screen.getByText("1,234,567")).toBeInTheDocument();
});
it("should filter data correctly when both toggles are off", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
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(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithMixed} />);
expect(screen.getAllByText("provider1").length).toBeGreaterThan(0);
expect(screen.queryByText("provider2")).not.toBeInTheDocument();
});
});
@@ -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<SpendByProviderProps> = ({ 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 (
<Card className="h-full">
<div className="flex justify-between items-center mb-4">
<Title>Spend by Provider</Title>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm text-gray-700">Show Zero Spend</label>
<Switch checked={includeZeroSpend} onChange={setIncludeZeroSpend} />
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<label className="text-sm text-gray-700">Show Unknown</label>
<Tooltip title="Requests that failed to route to a provider">
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
</Tooltip>
</div>
<Switch checked={includeUnknown} onChange={setIncludeUnknown} />
</div>
</div>
</div>
{loading ? (
<ChartLoader isDateChanging={isDateChanging} />
) : (
<Grid numItems={2}>
<Col numColSpan={1}>
<DonutChart
className="mt-4 h-40"
data={filteredProviderSpend}
index="provider"
category="spend"
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
colors={["cyan"]}
/>
</Col>
<Col numColSpan={1}>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{filteredProviderSpend.map((provider) => (
<TableRow key={provider.provider}>
<TableCell>
<div className="flex items-center space-x-2">
{provider.provider && <ProviderLogo provider={provider.provider} className="w-4 h-4" />}
<span>{provider.provider}</span>
</div>
</TableCell>
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
<TableCell className="text-green-600">
{provider.successful_requests.toLocaleString()}
</TableCell>
<TableCell className="text-red-600">
{provider.failed_requests.toLocaleString()}
</TableCell>
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Col>
</Grid>
)}
</Card>
);
};
export default SpendByProvider;
@@ -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: () => <div>Spend By Provider</div>,
}));
vi.mock("./EndpointUsage/EndpointUsage", () => ({
default: () => <div>Endpoint Usage</div>,
}));
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: () => <div>User Agent Activity</div>,
}));
@@ -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<typeof import("antd")>();
@@ -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(<NewUsagePage {...defaultProps} />);
renderWithProviders(<UsagePage {...defaultProps} />);
// Wait for data to be fetched
await waitFor(() => {
@@ -380,7 +536,7 @@ describe("NewUsage", () => {
});
it("should display usage metrics and charts", async () => {
renderWithProviders(<NewUsagePage {...defaultProps} />);
renderWithProviders(<UsagePage {...defaultProps} />);
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(<NewUsagePage {...defaultProps} />);
renderWithProviders(<UsagePage {...defaultProps} />);
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(<NewUsagePage {...defaultProps} organizations={mockOrganizations} />);
renderWithProviders(<UsagePage {...defaultProps} organizations={mockOrganizations} />);
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(<NewUsagePage {...defaultProps} />);
renderWithProviders(<UsagePage {...defaultProps} />);
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(<NewUsagePage {...defaultProps} />);
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
const usageSelect = screen.getByRole("combobox");
const usageSelect = screen.getByTestId("usage-view-select");
act(() => {
fireEvent.change(usageSelect, { target: { value: "agent" } });
});
@@ -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<UsagePageProps> = ({ teams, organizations }) => {
</Text>
</Card>
<Card>
<Title>Failed Requests</Title>
<div className="flex items-center gap-2">
<Title>Failed Requests</Title>
<Tooltip title="Includes requests that failed to route to a provider, tool usage failures, and other request errors where the provider cannot be determined.">
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
</Tooltip>
</div>
<Text className="text-2xl font-bold mt-2 text-red-600">
{userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0}
</Text>
@@ -669,79 +668,11 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{/* Spend by Provider */}
<Col numColSpan={2}>
<Card className="h-full">
<div className="flex justify-between items-center mb-4">
<Title>Spend by Provider</Title>
</div>
{loading ? (
<ChartLoader isDateChanging={isDateChanging} />
) : (
<Grid numItems={2}>
<Col numColSpan={1}>
<DonutChart
className="mt-4 h-40"
data={getProviderSpend()}
index="provider"
category="spend"
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
colors={["cyan"]}
/>
</Col>
<Col numColSpan={1}>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{getProviderSpend()
.filter((provider) => provider.spend > 0)
.map((provider) => (
<TableRow key={provider.provider}>
<TableCell>
<div className="flex items-center space-x-2">
{provider.provider && (
<img
src={getProviderLogoAndName(provider.provider).logo}
alt={`${provider.provider} logo`}
className="w-4 h-4"
onError={(e) => {
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);
}
}}
/>
)}
<span>{provider.provider}</span>
</div>
</TableCell>
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
<TableCell className="text-green-600">
{provider.successful_requests.toLocaleString()}
</TableCell>
<TableCell className="text-red-600">
{provider.failed_requests.toLocaleString()}
</TableCell>
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Col>
</Grid>
)}
</Card>
<SpendByProvider
loading={loading}
isDateChanging={isDateChanging}
providerSpend={getProviderSpend()}
/>
</Col>
{/* Usage Metrics */}