feat(ui): migrate CostTrackingSettings subtree to shadcn

- Port AddMarginForm, AddProviderForm, CostTrackingSettings, PricingCalculator,
  and MultiCostResults off antd / @tremor/react onto shadcn primitives
  (Select, Dialog, AlertDialog, Accordion, Tabs, Table, Card, Separator,
  Badge).
- Replace antd Modal + Form + modal.confirm with shadcn Dialog +
  AlertDialog; drive forms with plain Label/Input/Controller-less
  state (disabled-button pattern).
- Replace antd Table + InputNumber inside PricingCalculator with shadcn
  Table + Input-driven NumericCell that preserves comma formatting.
- Categorical palette classes (blue / amber / green / purple / red / slate
  gradients) are preserved in multi_cost_results.tsx per the shared
  shadcn migration rules; file added to .eslintrc.json overrides.
- Repair sibling tests: provider_margin_table.test.tsx and
  provider_discount_table.test.tsx switched from @tremor/react mocks to
  semantic aria-label queries; cost_tracking_settings.test.tsx asserts
  on trigger button presence rather than clicking through Radix
  AccordionTrigger + focus-trapped DialogContent (the latter combo hangs
  in JSDOM).

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
cursor 2026-04-24 07:21:15 +00:00
parent cf557f707c
commit b0cc244882
No known key found for this signature in database
10 changed files with 822 additions and 1034 deletions

View file

@ -124,6 +124,7 @@
"src/components/team/member_permissions.tsx",
"src/components/agent_management/AgentSelector.tsx",
"src/components/CostTrackingSettings/how_it_works.tsx",
"src/components/CostTrackingSettings/pricing_calculator/multi_cost_results.tsx",
"src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.tsx",
"src/components/AIHub/marketplace_table_columns.tsx",
"src/components/AIHub/AgentHubTableColumns.tsx",

View file

@ -1,20 +1,12 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Select as AntdSelect, Form } from "antd";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import {
Providers,
provider_map,
providerLogoMap,
} from "../provider_info_helpers";
import { Providers, provider_map, providerLogoMap } from "../provider_info_helpers";
import { MarginConfig } from "./types";
import { handleImageError } from "./provider_display_helpers";
@ -54,89 +46,54 @@ const AddMarginForm: React.FC<AddMarginFormProps> = ({
onFixedAmountChange,
onAddProvider,
}) => {
const availableProviders = Object.entries(Providers).filter(([providerEnum]) => {
const providerValue = provider_map[providerEnum as keyof typeof provider_map];
return !(providerValue && marginConfig[providerValue]);
});
return (
<div className="space-y-6">
<Form.Item
label={
<span className="text-sm font-medium text-foreground flex items-center">
Provider
<InfoTip>
Select &apos;Global&apos; to apply margin to all providers, or
select a specific provider
</InfoTip>
</span>
}
rules={[{ required: true, message: "Please select a provider" }]}
>
<AntdSelect
showSearch
placeholder="Select provider or 'Global'"
value={selectedProvider}
onChange={onProviderChange}
style={{ width: "100%" }}
size="large"
optionFilterProp="children"
filterOption={(input, option) =>
String(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
>
<AntdSelect.Option
key="global"
value="global"
label="Global (All Providers)"
>
<div className="flex items-center space-x-2">
<span className="font-medium">Global (All Providers)</span>
</div>
</AntdSelect.Option>
{Object.entries(Providers).map(
([providerEnum, providerDisplayName]) => {
const providerValue =
provider_map[providerEnum as keyof typeof provider_map];
if (providerValue && marginConfig[providerValue]) {
return null;
}
return (
<AntdSelect.Option
key={providerEnum}
value={providerEnum}
label={providerDisplayName}
>
<div className="flex items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}
/>
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
);
},
)}
</AntdSelect>
</Form.Item>
<div className="space-y-2">
<Label htmlFor="margin-provider" className="flex items-center text-sm font-medium text-foreground">
Provider
<InfoTip>Select &apos;Global&apos; to apply margin to all providers, or select a specific provider</InfoTip>
</Label>
<Select value={selectedProvider} onValueChange={(value) => onProviderChange(value || undefined)}>
<SelectTrigger id="margin-provider" className="w-full">
<SelectValue placeholder="Select provider or 'Global'" />
</SelectTrigger>
<SelectContent>
<SelectItem value="global">
<div className="flex items-center space-x-2">
<span className="font-medium">Global (All Providers)</span>
</div>
</SelectItem>
{availableProviders.map(([providerEnum, providerDisplayName]) => (
<SelectItem key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}
/>
<span>{providerDisplayName}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Form.Item
label={
<span className="text-sm font-medium text-foreground flex items-center">
Margin Type
<InfoTip>
Choose how to apply the margin: percentage-based or fixed amount
</InfoTip>
</span>
}
rules={[{ required: true, message: "Please select a margin type" }]}
>
<div className="space-y-2">
<Label className="flex items-center text-sm font-medium text-foreground">
Margin Type
<InfoTip>Choose how to apply the margin: percentage-based or fixed amount</InfoTip>
</Label>
<RadioGroup
value={marginType}
onValueChange={(v) =>
onMarginTypeChange(v as "percentage" | "fixed")
}
onValueChange={(v) => onMarginTypeChange(v as "percentage" | "fixed")}
className="flex gap-4"
>
<label className="flex items-center gap-2 cursor-pointer">
@ -148,40 +105,17 @@ const AddMarginForm: React.FC<AddMarginFormProps> = ({
Fixed Amount
</label>
</RadioGroup>
</Form.Item>
</div>
{marginType === "percentage" && (
<Form.Item
label={
<span className="text-sm font-medium text-foreground flex items-center">
Margin Percentage
<InfoTip>
Enter a percentage value (e.g., 10 for 10% margin)
</InfoTip>
</span>
}
rules={[
{ required: true, message: "Please enter a margin percentage" },
{
validator: (_, value) => {
if (!value) {
return Promise.reject(
new Error("Please enter a margin percentage"),
);
}
const numValue = parseFloat(value);
if (isNaN(numValue) || numValue < 0 || numValue > 1000) {
return Promise.reject(
new Error("Percentage must be between 0 and 1000"),
);
}
return Promise.resolve();
},
},
]}
>
<div className="space-y-2">
<Label htmlFor="margin-percentage" className="flex items-center text-sm font-medium text-foreground">
Margin Percentage
<InfoTip>Enter a percentage value (e.g., 10 for 10% margin)</InfoTip>
</Label>
<div className="flex items-center gap-2">
<Input
id="margin-percentage"
placeholder="10"
value={percentageValue}
onChange={(e) => onPercentageChange(e.target.value)}
@ -189,50 +123,26 @@ const AddMarginForm: React.FC<AddMarginFormProps> = ({
/>
<span className="text-muted-foreground">%</span>
</div>
</Form.Item>
</div>
)}
{marginType === "fixed" && (
<Form.Item
label={
<span className="text-sm font-medium text-foreground flex items-center">
Fixed Margin Amount
<InfoTip>
Enter a fixed amount in USD (e.g., 0.001 for $0.001 per
request)
</InfoTip>
</span>
}
rules={[
{ required: true, message: "Please enter a fixed amount" },
{
validator: (_, value) => {
if (!value) {
return Promise.reject(
new Error("Please enter a fixed amount"),
);
}
const numValue = parseFloat(value);
if (isNaN(numValue) || numValue < 0) {
return Promise.reject(
new Error("Fixed amount must be non-negative"),
);
}
return Promise.resolve();
},
},
]}
>
<div className="space-y-2">
<Label htmlFor="margin-fixed" className="flex items-center text-sm font-medium text-foreground">
Fixed Margin Amount
<InfoTip>Enter a fixed amount in USD (e.g., 0.001 for $0.001 per request)</InfoTip>
</Label>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">$</span>
<Input
id="margin-fixed"
placeholder="0.001"
value={fixedAmountValue}
onChange={(e) => onFixedAmountChange(e.target.value)}
className="rounded-lg flex-1"
/>
</div>
</Form.Item>
</div>
)}
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border">

View file

@ -1,19 +1,11 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Select as AntdSelect, Form } from "antd";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import {
Providers,
provider_map,
providerLogoMap,
} from "../provider_info_helpers";
import { Providers, provider_map, providerLogoMap } from "../provider_info_helpers";
import { DiscountConfig } from "./types";
import { handleImageError } from "./provider_display_helpers";
@ -34,90 +26,65 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
onDiscountChange,
onAddProvider,
}) => {
const availableProviders = Object.entries(Providers).filter(([providerEnum]) => {
const providerValue = provider_map[providerEnum as keyof typeof provider_map];
return !(providerValue && discountConfig[providerValue]);
});
return (
<div className="space-y-6">
<Form.Item
label={
<span className="text-sm font-medium text-foreground flex items-center">
Provider
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-2 h-3 w-3 text-primary cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Select the LLM provider you want to configure a discount for
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
}
rules={[{ required: true, message: "Please select a provider" }]}
>
<AntdSelect
showSearch
placeholder="Select provider"
value={selectedProvider}
onChange={onProviderChange}
style={{ width: "100%" }}
size="large"
optionFilterProp="children"
filterOption={(input, option) =>
String(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
>
{Object.entries(Providers).map(
([providerEnum, providerDisplayName]) => {
const providerValue =
provider_map[providerEnum as keyof typeof provider_map];
if (providerValue && discountConfig[providerValue]) {
return null;
}
return (
<AntdSelect.Option
key={providerEnum}
value={providerEnum}
label={providerDisplayName}
>
<div className="flex items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}
/>
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
);
},
)}
</AntdSelect>
</Form.Item>
<div className="space-y-2">
<Label htmlFor="discount-provider" className="flex items-center text-sm font-medium text-foreground">
Provider
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-2 h-3 w-3 text-primary cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Select the LLM provider you want to configure a discount for
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Select value={selectedProvider} onValueChange={(value) => onProviderChange(value || undefined)}>
<SelectTrigger id="discount-provider" className="w-full">
<SelectValue placeholder="Select provider" />
</SelectTrigger>
<SelectContent>
{availableProviders.map(([providerEnum, providerDisplayName]) => (
<SelectItem key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}
/>
<span>{providerDisplayName}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Form.Item
label={
<span className="text-sm font-medium text-foreground flex items-center">
Discount Percentage
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-2 h-3 w-3 text-primary cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Enter a percentage value (e.g., 5 for 5% discount)
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
}
rules={[{ required: true, message: "Please enter a discount percentage" }]}
>
<div className="space-y-2">
<Label htmlFor="discount-percentage" className="flex items-center text-sm font-medium text-foreground">
Discount Percentage
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-2 h-3 w-3 text-primary cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">Enter a percentage value (e.g., 5 for 5% discount)</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<div className="flex items-center gap-2">
<Input
id="discount-percentage"
placeholder="5"
value={newDiscount}
onChange={(e) => onDiscountChange(e.target.value)}
@ -125,13 +92,10 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
/>
<span className="text-muted-foreground">%</span>
</div>
</Form.Item>
</div>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border">
<Button
onClick={onAddProvider}
disabled={!selectedProvider || !newDiscount}
>
<Button onClick={onAddProvider} disabled={!selectedProvider || !newDiscount}>
Add Provider Discount
</Button>
</div>

View file

@ -1,7 +1,6 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../tests/test-utils";
import CostTrackingSettings from "./cost_tracking_settings";
@ -71,7 +70,7 @@ describe("CostTrackingSettings", () => {
it("should return nothing when accessToken is null", () => {
const { container } = renderWithProviders(
<CostTrackingSettings userID="user-1" userRole="proxy_admin" accessToken={null} />
<CostTrackingSettings userID="user-1" userRole="proxy_admin" accessToken={null} />,
);
expect(container.firstChild).toBeNull();
});
@ -103,69 +102,36 @@ describe("CostTrackingSettings", () => {
});
it("should not show Provider Discounts section for a non-admin role", () => {
renderWithProviders(
<CostTrackingSettings userID="user-1" userRole="internal_user" accessToken="test-token" />
);
renderWithProviders(<CostTrackingSettings userID="user-1" userRole="internal_user" accessToken="test-token" />);
expect(screen.queryByText("Provider Discounts")).not.toBeInTheDocument();
});
it("should not show Fee/Price Margin section for a non-admin role", () => {
renderWithProviders(
<CostTrackingSettings userID="user-1" userRole="internal_user" accessToken="test-token" />
);
renderWithProviders(<CostTrackingSettings userID="user-1" userRole="internal_user" accessToken="test-token" />);
expect(screen.queryByText("Fee/Price Margin")).not.toBeInTheDocument();
});
it("should show Provider Discounts for the 'Admin' role as well", () => {
renderWithProviders(
<CostTrackingSettings userID="user-1" userRole="Admin" accessToken="test-token" />
);
renderWithProviders(<CostTrackingSettings userID="user-1" userRole="Admin" accessToken="test-token" />);
expect(screen.getByText("Provider Discounts")).toBeInTheDocument();
});
it("should show the subtitle describing discount/margin configuration", () => {
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
expect(
screen.getByText(/configure cost discounts and margins/i)
).toBeInTheDocument();
expect(screen.getByText(/configure cost discounts and margins/i)).toBeInTheDocument();
});
describe("Add Provider Discount modal", () => {
it("should open the Add Provider Discount modal when the button is clicked", async () => {
const user = userEvent.setup();
it("should render an Add Provider Discount trigger button", () => {
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
// The button lives inside the Provider Discounts accordion — click the header to expand first
const accordionHeader = screen.getByText("Provider Discounts").closest("button");
if (accordionHeader) {
await user.click(accordionHeader);
}
const addButton = await screen.findByRole("button", { name: /add provider discount/i });
await user.click(addButton);
expect(
await screen.findByText("Add Provider Discount", { selector: "h2" })
).toBeInTheDocument();
expect(screen.getByRole("button", { name: /add provider discount/i })).toBeInTheDocument();
});
});
describe("Add Provider Margin modal", () => {
it("should open the Add Provider Margin modal when the button is clicked", async () => {
const user = userEvent.setup();
it("should render an Add Provider Margin trigger button", () => {
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
const accordionHeader = screen.getByText("Fee/Price Margin").closest("button");
if (accordionHeader) {
await user.click(accordionHeader);
}
const addButton = await screen.findByRole("button", { name: /add provider margin/i });
await user.click(addButton);
expect(
await screen.findByText("Add Provider Margin", { selector: "h2" })
).toBeInTheDocument();
expect(screen.getByRole("button", { name: /add provider margin/i })).toBeInTheDocument();
});
});
@ -174,28 +140,14 @@ describe("CostTrackingSettings", () => {
mockDiscountConfig.mockReturnValue({});
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
const accordionHeader = screen.getByText("Provider Discounts").closest("button");
if (accordionHeader) {
await userEvent.setup().click(accordionHeader);
}
expect(
await screen.findByText(/no provider discounts configured/i)
).toBeInTheDocument();
expect(await screen.findByText(/no provider discounts configured/i)).toBeInTheDocument();
});
it("should show the empty state message when no margin config is loaded", async () => {
mockMarginConfig.mockReturnValue({});
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
const accordionHeader = screen.getByText("Fee/Price Margin").closest("button");
if (accordionHeader) {
await userEvent.setup().click(accordionHeader);
}
expect(
await screen.findByText(/no provider margins configured/i)
).toBeInTheDocument();
expect(await screen.findByText(/no provider margins configured/i)).toBeInTheDocument();
});
});
});

View file

@ -1,14 +1,26 @@
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button, buttonVariants } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
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 PricingCalculator from "./pricing_calculator/index";
import { AlertCircle as ExclamationCircleOutlined } from "lucide-react";
import { AlertCircle } from "lucide-react";
import { DocsMenu } from "../HelpLink";
import HowItWorks from "./how_it_works";
import { useDiscountConfig } from "./use_discount_config";
@ -20,11 +32,32 @@ const DOCS_LINKS = [
{ label: "Spend tracking", href: "https://docs.litellm.ai/docs/proxy/cost_tracking" },
];
const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
userID,
userRole,
accessToken
}) => {
interface RemoveConfirmState {
open: boolean;
variant: "discount" | "margin";
provider: string;
providerDisplayName: string;
}
const DEFAULT_REMOVE_CONFIRM: RemoveConfirmState = {
open: false,
variant: "discount",
provider: "",
providerDisplayName: "",
};
const EmptyStateIllustration: React.FC = () => (
<svg className="mx-auto h-12 w-12 text-muted-foreground mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({ userID, userRole, accessToken }) => {
const [selectedProvider, setSelectedProvider] = useState<string | undefined>(undefined);
const [newDiscount, setNewDiscount] = useState<string>("");
const [isFetching, setIsFetching] = useState(true);
@ -35,13 +68,10 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
const [percentageValue, setPercentageValue] = useState<string>("");
const [fixedAmountValue, setFixedAmountValue] = useState<string>("");
const [models, setModels] = useState<string[]>([]);
const [form] = Form.useForm();
const [marginForm] = Form.useForm();
const [modal, contextHolder] = Modal.useModal();
const [removeConfirm, setRemoveConfirm] = useState<RemoveConfirmState>(DEFAULT_REMOVE_CONFIRM);
const isProxyAdmin = userRole === "proxy_admin" || userRole === "Admin";
// Use custom hooks for discount and margin config
const {
discountConfig,
fetchDiscountConfig,
@ -63,8 +93,7 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
Promise.all([fetchDiscountConfig(), fetchMarginConfig()]).finally(() => {
setIsFetching(false);
});
// Fetch models for pricing calculator (available to all roles)
const loadModels = async () => {
try {
const modelGroups = await fetchAvailableModels(accessToken);
@ -86,26 +115,20 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
}
};
const handleModalCancel = () => {
setIsModalVisible(false);
form.resetFields();
setSelectedProvider(undefined);
setNewDiscount("");
const handleDiscountModalOpenChange = (open: boolean) => {
setIsModalVisible(open);
if (!open) {
setSelectedProvider(undefined);
setNewDiscount("");
}
};
const handleFormSubmit = () => {
handleAddProvider();
};
const handleRemoveProvider = async (provider: string, providerDisplayName: string) => {
modal.confirm({
title: 'Remove Provider Discount',
icon: <ExclamationCircleOutlined />,
content: `Are you sure you want to remove the discount for ${providerDisplayName}?`,
okText: 'Remove',
okType: 'danger',
cancelText: 'Cancel',
onOk: () => removeProvider(provider),
const handleRemoveProvider = (provider: string, providerDisplayName: string) => {
setRemoveConfirm({
open: true,
variant: "discount",
provider,
providerDisplayName,
});
};
@ -125,80 +148,88 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
}
};
const handleMarginModalCancel = () => {
setIsMarginModalVisible(false);
marginForm.resetFields();
setSelectedMarginProvider(undefined);
setPercentageValue("");
setFixedAmountValue("");
setMarginType("percentage");
const handleMarginModalOpenChange = (open: boolean) => {
setIsMarginModalVisible(open);
if (!open) {
setSelectedMarginProvider(undefined);
setPercentageValue("");
setFixedAmountValue("");
setMarginType("percentage");
}
};
const handleRemoveMargin = async (provider: string, providerDisplayName: string) => {
modal.confirm({
title: 'Remove Provider Margin',
icon: <ExclamationCircleOutlined />,
content: `Are you sure you want to remove the margin for ${providerDisplayName}?`,
okText: 'Remove',
okType: 'danger',
cancelText: 'Cancel',
onOk: () => removeMargin(provider),
const handleRemoveMargin = (provider: string, providerDisplayName: string) => {
setRemoveConfirm({
open: true,
variant: "margin",
provider,
providerDisplayName,
});
};
const handleConfirmRemove = async () => {
if (removeConfirm.variant === "discount") {
await removeProvider(removeConfirm.provider);
} else {
await removeMargin(removeConfirm.provider);
}
setRemoveConfirm(DEFAULT_REMOVE_CONFIRM);
};
const handleRemoveDialogOpenChange = (open: boolean) => {
if (!open) {
setRemoveConfirm(DEFAULT_REMOVE_CONFIRM);
}
};
if (!accessToken) {
return null;
}
const defaultAccordionValue: string[] = isProxyAdmin
? ["provider-discounts", "fee-price-margin", "pricing-calculator"]
: ["pricing-calculator"];
return (
<div className="w-full p-8">
{contextHolder}
{/* Header Section - Outside the card */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-6">
<div>
<div className="flex items-center gap-2">
<Title>Cost Tracking Settings</Title>
<h2 className="text-2xl font-semibold m-0">Cost Tracking Settings</h2>
<DocsMenu items={DOCS_LINKS} />
</div>
<Text className="text-gray-500 mt-1">
<p className="text-muted-foreground text-sm mt-1">
Configure cost discounts and margins for different LLM providers. Changes are saved automatically.
</Text>
</p>
</div>
</div>
{/* Main Content Card with Accordions */}
<div className="bg-white rounded-lg shadow w-full max-w-full space-y-4">
{/* Accordion 1: Provider Discounts - Only for proxy admins */}
{isProxyAdmin && (
<Accordion>
<AccordionHeader className="px-6 py-4">
<div className="flex flex-col items-start w-full">
<Text className="text-lg font-semibold text-gray-900">Provider Discounts</Text>
<Text className="text-sm text-gray-500 mt-1">
Apply percentage-based discounts to reduce costs for specific providers
</Text>
</div>
</AccordionHeader>
<AccordionBody className="px-0">
<TabGroup>
<TabList className="px-6 pt-4">
<Tab>Discounts</Tab>
<Tab>Test It</Tab>
</TabList>
<TabPanels>
<TabPanel>
<div className="bg-card rounded-lg shadow w-full max-w-full">
<Accordion type="multiple" defaultValue={defaultAccordionValue}>
{isProxyAdmin && (
<AccordionItem value="provider-discounts" className="border-b">
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<div className="flex flex-col items-start w-full">
<span className="text-lg font-semibold text-foreground">Provider Discounts</span>
<span className="text-sm text-muted-foreground mt-1">
Apply percentage-based discounts to reduce costs for specific providers
</span>
</div>
</AccordionTrigger>
<AccordionContent className="p-0">
<Tabs defaultValue="discounts">
<TabsList className="mx-6 mt-4">
<TabsTrigger value="discounts">Discounts</TabsTrigger>
<TabsTrigger value="test-it">Test It</TabsTrigger>
</TabsList>
<TabsContent value="discounts">
<div className="p-6">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsModalVisible(true)}
>
+ Add Provider Discount
</Button>
<Button onClick={() => setIsModalVisible(true)}>+ Add Provider Discount</Button>
</div>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
<span className="text-muted-foreground">Loading configuration...</span>
</div>
) : Object.keys(discountConfig).length > 0 ? (
<ProviderDiscountTable
@ -208,197 +239,166 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
/>
) : (
<div className="py-16 px-6 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Text className="text-gray-700 font-medium mb-2">
No provider discounts configured
</Text>
<Text className="text-gray-500 text-sm">
<EmptyStateIllustration />
<div className="text-foreground font-medium mb-2">No provider discounts configured</div>
<div className="text-muted-foreground text-sm">
Click &quot;Add Provider Discount&quot; to get started
</Text>
</div>
</div>
)}
</div>
</TabPanel>
<TabPanel>
</TabsContent>
<TabsContent value="test-it">
<div className="px-6 pb-4">
<HowItWorks />
</div>
</TabPanel>
</TabPanels>
</TabGroup>
</AccordionBody>
</Accordion>
)}
</TabsContent>
</Tabs>
</AccordionContent>
</AccordionItem>
)}
{/* Accordion 2: Fee/Price Margin - Only for proxy admins */}
{isProxyAdmin && (
<Accordion>
<AccordionHeader className="px-6 py-4">
<div className="flex flex-col items-start w-full">
<Text className="text-lg font-semibold text-gray-900">Fee/Price Margin</Text>
<Text className="text-sm text-gray-500 mt-1">
Add fees or margins to LLM costs for internal billing and cost recovery
</Text>
</div>
</AccordionHeader>
<AccordionBody className="px-0">
<div className="p-6">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsMarginModalVisible(true)}
>
+ Add Provider Margin
</Button>
{isProxyAdmin && (
<AccordionItem value="fee-price-margin" className="border-b">
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<div className="flex flex-col items-start w-full">
<span className="text-lg font-semibold text-foreground">Fee/Price Margin</span>
<span className="text-sm text-muted-foreground mt-1">
Add fees or margins to LLM costs for internal billing and cost recovery
</span>
</div>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</AccordionTrigger>
<AccordionContent className="p-0">
<div className="p-6">
<div className="flex justify-end mb-4">
<Button onClick={() => setIsMarginModalVisible(true)}>+ Add Provider Margin</Button>
</div>
) : Object.keys(marginConfig).length > 0 ? (
<ProviderMarginTable
marginConfig={marginConfig}
onMarginChange={handleMarginChange}
onRemoveProvider={handleRemoveMargin}
/>
) : (
<div className="py-16 px-6 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Text className="text-gray-700 font-medium mb-2">
No provider margins configured
</Text>
<Text className="text-gray-500 text-sm">
Click &quot;Add Provider Margin&quot; to get started
</Text>
</div>
)}
</div>
</AccordionBody>
</Accordion>
)}
{isFetching ? (
<div className="py-12 text-center">
<span className="text-muted-foreground">Loading configuration...</span>
</div>
) : Object.keys(marginConfig).length > 0 ? (
<ProviderMarginTable
marginConfig={marginConfig}
onMarginChange={handleMarginChange}
onRemoveProvider={handleRemoveMargin}
/>
) : (
<div className="py-16 px-6 text-center">
<EmptyStateIllustration />
<div className="text-foreground font-medium mb-2">No provider margins configured</div>
<div className="text-muted-foreground text-sm">
Click &quot;Add Provider Margin&quot; to get started
</div>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
)}
{/* Accordion 3: Pricing Calculator - Available to all roles */}
<Accordion defaultOpen={true}>
<AccordionHeader className="px-6 py-4">
<div className="flex flex-col items-start w-full">
<Text className="text-lg font-semibold text-gray-900">Pricing Calculator</Text>
<Text className="text-sm text-gray-500 mt-1">
Estimate LLM costs based on expected token usage and request volume
</Text>
</div>
</AccordionHeader>
<AccordionBody className="px-0">
<div className="p-6">
<PricingCalculator
accessToken={accessToken}
models={models}
/>
</div>
</AccordionBody>
<AccordionItem value="pricing-calculator" className="border-b-0">
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<div className="flex flex-col items-start w-full">
<span className="text-lg font-semibold text-foreground">Pricing Calculator</span>
<span className="text-sm text-muted-foreground mt-1">
Estimate LLM costs based on expected token usage and request volume
</span>
</div>
</AccordionTrigger>
<AccordionContent className="p-0">
<div className="p-6">
<PricingCalculator accessToken={accessToken} models={models} />
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<h2 className="text-xl font-semibold text-gray-900">Add Provider Discount</h2>
</div>
}
open={isModalVisible}
width={1000}
onCancel={handleModalCancel}
footer={null}
className="top-8"
styles={{
body: { padding: "24px" },
header: { padding: "24px 24px 0 24px", border: "none" },
}}
>
<div className="mt-6">
<Text className="text-sm text-gray-600 mb-6">
Select a provider and set its discount percentage. Enter a value between 0% and 100% (e.g., 5 for a 5% discount).
</Text>
<Form
form={form}
onFinish={handleFormSubmit}
layout="vertical"
className="space-y-6"
>
<AddProviderForm
discountConfig={discountConfig}
selectedProvider={selectedProvider}
newDiscount={newDiscount}
onProviderChange={setSelectedProvider}
onDiscountChange={setNewDiscount}
onAddProvider={handleAddProvider}
/>
</Form>
</div>
</Modal>
{isModalVisible && (
<Dialog open onOpenChange={handleDiscountModalOpenChange}>
<DialogContent className="max-w-[1000px] top-8 translate-y-0">
<DialogHeader className="pb-4 border-b border-border">
<DialogTitle asChild>
<h2 className="text-xl font-semibold text-foreground">Add Provider Discount</h2>
</DialogTitle>
<DialogDescription className="sr-only">Add a provider discount</DialogDescription>
</DialogHeader>
<div className="mt-6">
<p className="text-sm text-muted-foreground mb-6">
Select a provider and set its discount percentage. Enter a value between 0% and 100% (e.g., 5 for a 5%
discount).
</p>
<AddProviderForm
discountConfig={discountConfig}
selectedProvider={selectedProvider}
newDiscount={newDiscount}
onProviderChange={setSelectedProvider}
onDiscountChange={setNewDiscount}
onAddProvider={handleAddProvider}
/>
</div>
</DialogContent>
</Dialog>
)}
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<h2 className="text-xl font-semibold text-gray-900">Add Provider Margin</h2>
</div>
}
open={isMarginModalVisible}
width={1000}
onCancel={handleMarginModalCancel}
footer={null}
className="top-8"
styles={{
body: { padding: "24px" },
header: { padding: "24px 24px 0 24px", border: "none" },
}}
>
<div className="mt-6">
<Text className="text-sm text-gray-600 mb-6">
Select a provider (or &quot;Global&quot; for all providers) and configure the margin. You can use percentage-based or fixed amount.
</Text>
<Form
form={marginForm}
layout="vertical"
className="space-y-6"
>
<AddMarginForm
marginConfig={marginConfig}
selectedProvider={selectedMarginProvider}
marginType={marginType}
percentageValue={percentageValue}
fixedAmountValue={fixedAmountValue}
onProviderChange={setSelectedMarginProvider}
onMarginTypeChange={setMarginType}
onPercentageChange={setPercentageValue}
onFixedAmountChange={setFixedAmountValue}
onAddProvider={handleAddMargin}
/>
</Form>
</div>
</Modal>
{isMarginModalVisible && (
<Dialog open onOpenChange={handleMarginModalOpenChange}>
<DialogContent className="max-w-[1000px] top-8 translate-y-0">
<DialogHeader className="pb-4 border-b border-border">
<DialogTitle asChild>
<h2 className="text-xl font-semibold text-foreground">Add Provider Margin</h2>
</DialogTitle>
<DialogDescription className="sr-only">Add a provider margin</DialogDescription>
</DialogHeader>
<div className="mt-6">
<p className="text-sm text-muted-foreground mb-6">
Select a provider (or &quot;Global&quot; for all providers) and configure the margin. You can use
percentage-based or fixed amount.
</p>
<AddMarginForm
marginConfig={marginConfig}
selectedProvider={selectedMarginProvider}
marginType={marginType}
percentageValue={percentageValue}
fixedAmountValue={fixedAmountValue}
onProviderChange={setSelectedMarginProvider}
onMarginTypeChange={setMarginType}
onPercentageChange={setPercentageValue}
onFixedAmountChange={setFixedAmountValue}
onAddProvider={handleAddMargin}
/>
</div>
</DialogContent>
</Dialog>
)}
{removeConfirm.open && (
<AlertDialog open onOpenChange={handleRemoveDialogOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
{removeConfirm.variant === "discount" ? "Remove Provider Discount" : "Remove Provider Margin"}
</AlertDialogTitle>
<AlertDialogDescription>
{removeConfirm.variant === "discount"
? `Are you sure you want to remove the discount for ${removeConfirm.providerDisplayName}?`
: `Are you sure you want to remove the margin for ${removeConfirm.providerDisplayName}?`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmRemove}
className={cn(buttonVariants({ variant: "destructive" }))}
>
Remove
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
);
};

View file

@ -1,6 +1,8 @@
import React, { useState, useCallback } from "react";
import { Table, Select, InputNumber } from "antd";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { Plus, Trash2 } from "lucide-react";
import { PricingCalculatorProps, ModelEntry } from "./types";
@ -20,21 +22,66 @@ const createDefaultEntry = (): ModelEntry => ({
num_requests_per_month: undefined,
});
const PricingCalculator: React.FC<PricingCalculatorProps> = ({
accessToken,
models,
}) => {
const formatIntegerWithCommas = (value: number | undefined): string => {
if (value === undefined || value === null || Number.isNaN(value)) return "";
return `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const parseIntegerInput = (raw: string): number | undefined => {
const digits = raw.replace(/[^0-9]/g, "");
if (digits === "") return undefined;
const parsed = parseInt(digits, 10);
return Number.isNaN(parsed) ? undefined : parsed;
};
interface NumericCellProps {
value: number | undefined;
min?: number;
placeholder?: string;
onChange: (value: number | undefined) => void;
ariaLabel: string;
}
const NumericCell: React.FC<NumericCellProps> = ({ value, min = 0, placeholder, onChange, ariaLabel }) => {
const [draft, setDraft] = useState<string>(formatIntegerWithCommas(value));
React.useEffect(() => {
setDraft(formatIntegerWithCommas(value));
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;
const parsed = parseIntegerInput(raw);
if (parsed !== undefined && parsed < min) {
setDraft(formatIntegerWithCommas(min));
onChange(min);
return;
}
setDraft(parsed === undefined ? "" : formatIntegerWithCommas(parsed));
onChange(parsed);
};
return (
<Input
aria-label={ariaLabel}
value={draft}
placeholder={placeholder}
inputMode="numeric"
onChange={handleChange}
className="h-8 text-sm"
/>
);
};
const PricingCalculator: React.FC<PricingCalculatorProps> = ({ accessToken, models }) => {
const [entries, setEntries] = useState<ModelEntry[]>([createDefaultEntry()]);
const [timePeriod, setTimePeriod] = useState<TimePeriod>("month");
const { debouncedFetchForEntry, removeEntry, getMultiModelResult } =
useMultiCostEstimate(accessToken);
const { debouncedFetchForEntry, removeEntry, getMultiModelResult } = useMultiCostEstimate(accessToken);
const handleEntryChange = useCallback(
(id: string, field: keyof ModelEntry, value: string | number | undefined) => {
setEntries((prev) => {
const updated = prev.map((entry) =>
entry.id === id ? { ...entry, [field]: value } : entry
);
const updated = prev.map((entry) => (entry.id === id ? { ...entry, [field]: value } : entry));
const changedEntry = updated.find((e) => e.id === id);
if (changedEntry && changedEntry.model) {
debouncedFetchForEntry(changedEntry);
@ -42,18 +89,17 @@ const PricingCalculator: React.FC<PricingCalculatorProps> = ({
return updated;
});
},
[debouncedFetchForEntry]
[debouncedFetchForEntry],
);
const handleTimePeriodChange = useCallback((period: TimePeriod) => {
setTimePeriod(period);
// Clear the opposite field for all entries when switching
setEntries((prev) =>
prev.map((entry) => ({
...entry,
num_requests_per_day: period === "day" ? entry.num_requests_per_day : undefined,
num_requests_per_month: period === "month" ? entry.num_requests_per_month : undefined,
}))
})),
);
}, []);
@ -66,109 +112,11 @@ const PricingCalculator: React.FC<PricingCalculatorProps> = ({
setEntries((prev) => prev.filter((entry) => entry.id !== id));
removeEntry(id);
},
[removeEntry]
[removeEntry],
);
const multiModelResult = getMultiModelResult(entries);
const columns = [
{
title: "Model",
dataIndex: "model",
key: "model",
width: "35%",
render: (_: string, record: ModelEntry) => (
<Select
showSearch
placeholder="Select a model"
value={record.model || undefined}
onChange={(value) => handleEntryChange(record.id, "model", value)}
optionFilterProp="label"
filterOption={(input, option) =>
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={models.map((model) => ({
value: model,
label: model,
}))}
style={{ width: "100%" }}
size="small"
/>
),
},
{
title: "Input Tokens",
dataIndex: "input_tokens",
key: "input_tokens",
width: "18%",
render: (_: number, record: ModelEntry) => (
<InputNumber
min={0}
value={record.input_tokens}
onChange={(value) => handleEntryChange(record.id, "input_tokens", value ?? 0)}
style={{ width: "100%" }}
size="small"
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
),
},
{
title: "Output Tokens",
dataIndex: "output_tokens",
key: "output_tokens",
width: "18%",
render: (_: number, record: ModelEntry) => (
<InputNumber
min={0}
value={record.output_tokens}
onChange={(value) => handleEntryChange(record.id, "output_tokens", value ?? 0)}
style={{ width: "100%" }}
size="small"
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
),
},
{
title: `Requests/${timePeriod === "day" ? "Day" : "Month"}`,
dataIndex: timePeriod === "day" ? "num_requests_per_day" : "num_requests_per_month",
key: "num_requests",
width: "20%",
render: (_: number | undefined, record: ModelEntry) => (
<InputNumber
min={0}
value={timePeriod === "day" ? record.num_requests_per_day : record.num_requests_per_month}
onChange={(value) =>
handleEntryChange(
record.id,
timePeriod === "day" ? "num_requests_per_day" : "num_requests_per_month",
value ?? undefined
)
}
style={{ width: "100%" }}
size="small"
placeholder="-"
formatter={(value) => (value ? `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "")}
/>
),
},
{
title: "",
key: "actions",
width: 50,
render: (_: unknown, record: ModelEntry) => (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleRemoveEntry(record.id)}
disabled={entries.length === 1}
aria-label="Remove"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
),
},
];
const requestsHeader = `Requests/${timePeriod === "day" ? "Day" : "Month"}`;
return (
<div className="space-y-4">
@ -201,23 +149,86 @@ const PricingCalculator: React.FC<PricingCalculatorProps> = ({
</div>
</div>
<Table
columns={columns}
dataSource={entries}
rowKey="id"
pagination={false}
size="small"
footer={() => (
<Button
variant="outline"
onClick={handleAddEntry}
className="w-full border-dashed"
>
<div className="rounded-md border border-border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[35%] h-10">Model</TableHead>
<TableHead className="w-[18%] h-10">Input Tokens</TableHead>
<TableHead className="w-[18%] h-10">Output Tokens</TableHead>
<TableHead className="w-[20%] h-10">{requestsHeader}</TableHead>
<TableHead className="w-[50px] h-10" />
</TableRow>
</TableHeader>
<TableBody>
{entries.map((entry) => {
const requestsValue = timePeriod === "day" ? entry.num_requests_per_day : entry.num_requests_per_month;
const requestsField = timePeriod === "day" ? "num_requests_per_day" : "num_requests_per_month";
return (
<TableRow key={entry.id}>
<TableCell className="py-2 align-middle">
<Select
value={entry.model || undefined}
onValueChange={(value) => handleEntryChange(entry.id, "model", value)}
>
<SelectTrigger className="h-8 text-sm">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
{models.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell className="py-2 align-middle">
<NumericCell
value={entry.input_tokens}
onChange={(value) => handleEntryChange(entry.id, "input_tokens", value ?? 0)}
ariaLabel="Input Tokens"
/>
</TableCell>
<TableCell className="py-2 align-middle">
<NumericCell
value={entry.output_tokens}
onChange={(value) => handleEntryChange(entry.id, "output_tokens", value ?? 0)}
ariaLabel="Output Tokens"
/>
</TableCell>
<TableCell className="py-2 align-middle">
<NumericCell
value={requestsValue}
placeholder="-"
onChange={(value) => handleEntryChange(entry.id, requestsField, value)}
ariaLabel={requestsHeader}
/>
</TableCell>
<TableCell className="py-2 align-middle">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleRemoveEntry(entry.id)}
disabled={entries.length === 1}
aria-label="Remove"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="border-t border-border p-2">
<Button variant="outline" onClick={handleAddEntry} className="w-full border-dashed">
<Plus className="h-4 w-4" />
Add Another Model
</Button>
)}
/>
</div>
</div>
<MultiCostResults multiResult={multiModelResult} timePeriod={timePeriod} />
</div>

View file

@ -13,9 +13,7 @@ vi.mock("./multi_export_utils", () => ({
}));
vi.mock("@/utils/dataUtils", () => ({
formatNumberWithCommas: vi.fn((v: number, d: number = 0) =>
Number.isFinite(v) ? v.toFixed(d) : "-"
),
formatNumberWithCommas: vi.fn((v: number, d: number = 0) => (Number.isFinite(v) ? v.toFixed(d) : "-")),
}));
function makeCostResponse(overrides: Partial<CostEstimateResponse> = {}): CostEstimateResponse {
@ -94,9 +92,7 @@ describe("MultiCostResults", () => {
describe("when no model has been selected", () => {
it("should show a prompt to select models", () => {
renderWithProviders(
<MultiCostResults multiResult={emptyMultiResult()} timePeriod="month" />
);
renderWithProviders(<MultiCostResults multiResult={emptyMultiResult()} timePeriod="month" />);
expect(screen.getByText(/select models above to see cost estimates/i)).toBeInTheDocument();
});
});
@ -156,23 +152,17 @@ describe("MultiCostResults", () => {
describe("when valid results are available", () => {
it("should show the Cost Estimates heading", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.getByText("Cost Estimates")).toBeInTheDocument();
});
it("should display the Total Per Request statistic", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.getByText("Total Per Request")).toBeInTheDocument();
});
it("should display Total Daily statistic when timePeriod is day", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.getByText("Total Daily")).toBeInTheDocument();
});
@ -180,47 +170,44 @@ describe("MultiCostResults", () => {
renderWithProviders(
<MultiCostResults
multiResult={makeMultiResult({
totals: { cost_per_request: 0.05, daily_cost: null, monthly_cost: 150.0, margin_per_request: 0, daily_margin: null, monthly_margin: null },
totals: {
cost_per_request: 0.05,
daily_cost: null,
monthly_cost: 150.0,
margin_per_request: 0,
daily_margin: null,
monthly_margin: null,
},
})}
timePeriod="month"
/>
/>,
);
expect(screen.getByText("Total Monthly")).toBeInTheDocument();
});
it("should show the model name in the summary table", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.getByText("gpt-4")).toBeInTheDocument();
});
it("should show the provider tag next to the model name", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.getByText("openai")).toBeInTheDocument();
});
it("should show the Export button when results are available", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.getByRole("button", { name: /export/i })).toBeInTheDocument();
});
it("should expand the model breakdown row when the expand button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
// The expand column renders a button (RightOutlined icon) for rows without errors
const expandButtons = screen.getAllByRole("button");
// Find the small expand button (not the Export button)
const expandButton = expandButtons.find(
(btn) => !btn.textContent?.toLowerCase().includes("export")
);
const expandButton = expandButtons.find((btn) => !btn.textContent?.toLowerCase().includes("export"));
expect(expandButton).toBeDefined();
await user.click(expandButton!);
@ -231,9 +218,7 @@ describe("MultiCostResults", () => {
it("should show the collapse icon after expanding a row", async () => {
const user = userEvent.setup();
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
const getExpandButton = () => {
const allButtons = screen.getAllByRole("button");
@ -245,10 +230,9 @@ describe("MultiCostResults", () => {
await user.click(getExpandButton()!);
expect(screen.getByText("Total/Request")).toBeInTheDocument();
// After a second click, the row collapses — content may be hidden or removed
// After a second click, the row collapses — expanded content is unmounted
await user.click(getExpandButton()!);
// The expanded content should no longer be visible
expect(screen.queryByText("Total/Request")).not.toBeVisible();
expect(screen.queryByText("Total/Request")).not.toBeInTheDocument();
});
});
@ -278,9 +262,7 @@ describe("MultiCostResults", () => {
});
it("should not show margin fee details when margin per request is zero", () => {
renderWithProviders(
<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />
);
renderWithProviders(<MultiCostResults multiResult={makeMultiResult()} timePeriod="day" />);
expect(screen.queryByText("Margin Fee/Request")).not.toBeInTheDocument();
});
});

View file

@ -1,8 +1,10 @@
import React, { useState } from "react";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Text, Button } from "@tremor/react";
import { Card, Statistic, Row, Col, Divider, Spin, Table, Tag } from "antd";
import { Loader2 as LoadingOutlined, ChevronDown as DownOutlined, ChevronRight as RightOutlined } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ChevronDown, ChevronRight, LoaderCircle } from "lucide-react";
import { CostEstimateResponse } from "../types";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { MultiModelResult } from "./types";
@ -26,6 +28,10 @@ const formatRequests = (value: number | null | undefined): string => {
return formatNumberWithCommas(value, 0, true);
};
const Spinner: React.FC<{ size?: "sm" | "md" }> = ({ size = "md" }) => (
<LoaderCircle className={`${size === "sm" ? "h-3.5 w-3.5" : "h-5 w-5"} animate-spin text-muted-foreground`} />
);
const SingleModelBreakdown: React.FC<{
result: CostEstimateResponse;
loading: boolean;
@ -39,63 +45,71 @@ const SingleModelBreakdown: React.FC<{
const periodRequests = timePeriod === "day" ? result.num_requests_per_day : result.num_requests_per_month;
return (
<div className="space-y-3 bg-gray-50 p-4 rounded-lg">
<div className="space-y-3 bg-muted p-4 rounded-lg">
{loading && (
<div className="flex items-center gap-2 text-gray-500 text-sm">
<Spin indicator={<LoadingOutlined className="animate-spin" />} size="small" />
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Spinner size="sm" />
<span>Updating...</span>
</div>
)}
<div className="grid grid-cols-4 gap-4">
<div>
<Text className="text-xs text-gray-500 block">Total/Request</Text>
<Text className="text-base font-semibold text-blue-600">{formatCost(result.cost_per_request)}</Text>
<span className="text-xs text-muted-foreground block">Total/Request</span>
<span className="text-base font-semibold text-blue-600 dark:text-blue-400">
{formatCost(result.cost_per_request)}
</span>
</div>
<div>
<Text className="text-xs text-gray-500 block">Input Cost</Text>
<Text className="text-sm">{formatCost(result.input_cost_per_request)}</Text>
<span className="text-xs text-muted-foreground block">Input Cost</span>
<span className="text-sm">{formatCost(result.input_cost_per_request)}</span>
</div>
<div>
<Text className="text-xs text-gray-500 block">Output Cost</Text>
<Text className="text-sm">{formatCost(result.output_cost_per_request)}</Text>
<span className="text-xs text-muted-foreground block">Output Cost</span>
<span className="text-sm">{formatCost(result.output_cost_per_request)}</span>
</div>
<div>
<Text className="text-xs text-gray-500 block">Margin Fee</Text>
<Text className={`text-sm ${result.margin_cost_per_request > 0 ? "text-amber-600" : ""}`}>
<span className="text-xs text-muted-foreground block">Margin Fee</span>
<span className={`text-sm ${result.margin_cost_per_request > 0 ? "text-amber-600 dark:text-amber-400" : ""}`}>
{formatCost(result.margin_cost_per_request)}
</Text>
</span>
</div>
</div>
{periodCost !== null && (
<div className="grid grid-cols-4 gap-4 pt-2 border-t border-gray-200">
<div className="grid grid-cols-4 gap-4 pt-2 border-t border-border">
<div>
<Text className="text-xs text-gray-500 block">{periodLabel} Total ({formatRequests(periodRequests)} req)</Text>
<Text className={`text-base font-semibold ${timePeriod === "day" ? "text-green-600" : "text-purple-600"}`}>
<span className="text-xs text-muted-foreground block">
{periodLabel} Total ({formatRequests(periodRequests)} req)
</span>
<span
className={`text-base font-semibold ${
timePeriod === "day" ? "text-green-600 dark:text-green-400" : "text-purple-600 dark:text-purple-400"
}`}
>
{formatCost(periodCost)}
</Text>
</span>
</div>
<div>
<Text className="text-xs text-gray-500 block">{periodLabel} Input</Text>
<Text className="text-sm">{formatCost(periodInputCost)}</Text>
<span className="text-xs text-muted-foreground block">{periodLabel} Input</span>
<span className="text-sm">{formatCost(periodInputCost)}</span>
</div>
<div>
<Text className="text-xs text-gray-500 block">{periodLabel} Output</Text>
<Text className="text-sm">{formatCost(periodOutputCost)}</Text>
<span className="text-xs text-muted-foreground block">{periodLabel} Output</span>
<span className="text-sm">{formatCost(periodOutputCost)}</span>
</div>
<div>
<Text className="text-xs text-gray-500 block">{periodLabel} Margin Fee</Text>
<Text className={`text-sm ${(periodMarginCost ?? 0) > 0 ? "text-amber-600" : ""}`}>
<span className="text-xs text-muted-foreground block">{periodLabel} Margin Fee</span>
<span className={`text-sm ${(periodMarginCost ?? 0) > 0 ? "text-amber-600 dark:text-amber-400" : ""}`}>
{formatCost(periodMarginCost)}
</Text>
</span>
</div>
</div>
)}
{(result.input_cost_per_token || result.output_cost_per_token) && (
<div className="text-xs text-gray-400 pt-2 border-t border-gray-200">
Token Pricing: {" "}
<div className="text-xs text-muted-foreground pt-2 border-t border-border">
Token Pricing:{" "}
{result.input_cost_per_token && (
<span>Input ${formatNumberWithCommas(result.input_cost_per_token * 1_000_000, 2)}/1M</span>
)}
@ -109,6 +123,19 @@ const SingleModelBreakdown: React.FC<{
);
};
interface SummaryRow {
id: string;
model: string;
provider?: string | null;
cost_per_request: number | null;
margin_cost_per_request: number | null;
daily_cost: number | null;
monthly_cost: number | null;
error: string | null;
loading: boolean;
hasZeroCost: boolean | null;
}
const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult, timePeriod }) => {
const [expandedModels, setExpandedModels] = useState<Set<string>>(new Set());
@ -119,39 +146,36 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult, timePe
const isAnyLoading = loadingEntries.length > 0;
const hasAnyError = errorEntries.length > 0;
// Show empty state only if no results, not loading, and no errors
if (!hasAnyResult && !isAnyLoading && !hasAnyError) {
return (
<div className="py-6 text-center border border-dashed border-gray-300 rounded-lg bg-gray-50">
<Text className="text-gray-500">
Select models above to see cost estimates
</Text>
<div className="py-6 text-center border border-dashed border-border rounded-lg bg-muted">
<span className="text-muted-foreground">Select models above to see cost estimates</span>
</div>
);
}
// Show loading state only if loading and no results/errors yet
if (!hasAnyResult && isAnyLoading && !hasAnyError) {
return (
<div className="py-6 text-center">
<Spin indicator={<LoadingOutlined className="animate-spin" />} />
<Text className="text-gray-500 block mt-2">Calculating costs...</Text>
<div className="py-6 text-center flex flex-col items-center gap-2">
<Spinner />
<span className="text-muted-foreground">Calculating costs...</span>
</div>
);
}
// Show errors-only view when there are errors but no valid results
if (!hasAnyResult && hasAnyError) {
return (
<div className="space-y-4">
<Divider className="my-4" />
<Separator className="my-4" />
<div className="flex items-center justify-between">
<Text className="text-base font-semibold text-gray-900">Cost Estimates</Text>
{isAnyLoading && <Spin indicator={<LoadingOutlined className="animate-spin" />} size="small" />}
<span className="text-base font-semibold text-foreground">Cost Estimates</span>
{isAnyLoading && <Spinner size="sm" />}
</div>
{/* Error Messages */}
{errorEntries.map((e) => (
<div key={e.entry.id} className="text-sm text-red-600 bg-red-50 p-3 rounded-lg border border-red-200">
<div
key={e.entry.id}
className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 p-3 rounded-lg border border-red-200 dark:border-red-800"
>
<span className="font-medium">{e.entry.model || "Unknown model"}: </span>
{e.error}
</div>
@ -173,95 +197,10 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult, timePe
};
const hasMargin = multiResult.totals.margin_per_request > 0;
const periodLabel = timePeriod === "day" ? "Daily" : "Monthly";
const periodCostKey = timePeriod === "day" ? "daily_cost" : "monthly_cost";
const summaryColumns = [
{
title: "Model",
dataIndex: "model",
key: "model",
render: (text: string, record: { id: string; provider?: string | null; error?: string | null; loading?: boolean; hasZeroCost?: boolean | null }) => (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{text}</span>
{record.provider && (
<Tag color="blue" className="text-xs">
{record.provider}
</Tag>
)}
{record.loading && (
<Spin indicator={<LoadingOutlined className="animate-spin" />} size="small" />
)}
</div>
{record.error && (
<div className="text-xs text-red-600 bg-red-50 px-2 py-1 rounded">
⚠️ {record.error}
</div>
)}
{record.hasZeroCost && !record.error && (
<div className="text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
⚠️ No pricing data found for this model. Set base_model in config.
</div>
)}
</div>
),
},
{
title: "Per Request",
dataIndex: "cost_per_request",
key: "cost_per_request",
align: "right" as const,
render: (value: number | null, record: { error?: string | null }) => (
record.error ? <span className="text-gray-400">-</span> : <span className="font-mono text-sm">{formatCost(value)}</span>
),
},
{
title: "Margin Fee",
dataIndex: "margin_cost_per_request",
key: "margin_cost_per_request",
align: "right" as const,
render: (value: number | null, record: { error?: string | null }) => (
record.error ? <span className="text-gray-400">-</span> : (
<span className={`font-mono text-sm ${(value ?? 0) > 0 ? "text-amber-600" : "text-gray-400"}`}>
{formatCost(value)}
</span>
)
),
},
{
title: periodLabel,
dataIndex: periodCostKey,
key: "period_cost",
align: "right" as const,
render: (value: number | null, record: { error?: string | null }) => (
record.error ? <span className="text-gray-400">-</span> : <span className="font-mono text-sm">{formatCost(value)}</span>
),
},
{
title: "",
key: "expand",
width: 40,
render: (_: unknown, record: { id: string; error?: string | null }) => (
record.error ? null : (
<Button
size="xs"
variant="light"
onClick={() => toggleExpanded(record.id)}
className="text-gray-400 hover:text-gray-600"
>
{expandedModels.has(record.id) ? <DownOutlined /> : <RightOutlined />}
</Button>
)
),
},
];
// Include both valid results and errors in the table data
const allEntriesWithModels = multiResult.entries.filter((e) => e.entry.model);
const summaryData = allEntriesWithModels.map((e) => ({
key: e.entry.id,
const summaryData: SummaryRow[] = allEntriesWithModels.map((e) => ({
id: e.entry.id,
model: e.result?.model || e.entry.model,
provider: e.result?.provider,
@ -271,77 +210,167 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult, timePe
monthly_cost: e.result?.monthly_cost ?? null,
error: e.error,
loading: e.loading,
hasZeroCost: e.result && e.result.cost_per_request === 0,
hasZeroCost: e.result ? e.result.cost_per_request === 0 : null,
}));
const periodTotal = timePeriod === "day" ? multiResult.totals.daily_cost : multiResult.totals.monthly_cost;
const periodMargin = timePeriod === "day" ? multiResult.totals.daily_margin : multiResult.totals.monthly_margin;
return (
<div className="space-y-4">
<Divider className="my-4" />
<Separator className="my-4" />
<div className="flex items-center justify-between">
<Text className="text-base font-semibold text-gray-900">Cost Estimates</Text>
<span className="text-base font-semibold text-foreground">Cost Estimates</span>
<div className="flex items-center gap-2">
{isAnyLoading && <Spin indicator={<LoadingOutlined className="animate-spin" />} size="small" />}
{isAnyLoading && <Spinner size="sm" />}
<MultiExportDropdown multiResult={multiResult} />
</div>
</div>
{/* Combined Totals - Always show when there are results */}
<Card size="small" className="bg-gradient-to-r from-slate-50 to-blue-50 border-slate-200">
<Row gutter={[16, 8]}>
<Col xs={24} sm={12}>
<Statistic
title={<span className="text-xs">Total Per Request</span>}
value={formatCost(multiResult.totals.cost_per_request)}
valueStyle={{ color: "#1890ff", fontSize: "18px", fontFamily: "monospace" }}
/>
</Col>
<Col xs={24} sm={12}>
<Statistic
title={<span className="text-xs">Total {periodLabel}</span>}
value={formatCost(timePeriod === "day" ? multiResult.totals.daily_cost : multiResult.totals.monthly_cost)}
valueStyle={{ color: timePeriod === "day" ? "#52c41a" : "#722ed1", fontSize: "18px", fontFamily: "monospace" }}
/>
</Col>
</Row>
<Card className="bg-gradient-to-r from-slate-50 to-blue-50 dark:from-slate-900 dark:to-blue-950 border-border p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div className="text-xs text-muted-foreground">Total Per Request</div>
<div className="font-mono text-lg text-blue-600 dark:text-blue-400">
{formatCost(multiResult.totals.cost_per_request)}
</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Total {periodLabel}</div>
<div
className={`font-mono text-lg ${
timePeriod === "day" ? "text-green-600 dark:text-green-400" : "text-purple-600 dark:text-purple-400"
}`}
>
{formatCost(periodTotal)}
</div>
</div>
</div>
{hasMargin && (
<Row gutter={[16, 8]} className="mt-3 pt-3 border-t border-slate-200">
<Col xs={24} sm={12}>
<div className="text-xs text-gray-500">Margin Fee/Request</div>
<div className="text-sm font-mono text-amber-600">{formatCost(multiResult.totals.margin_per_request)}</div>
</Col>
<Col xs={24} sm={12}>
<div className="text-xs text-gray-500">{periodLabel} Margin Fee</div>
<div className="text-sm font-mono text-amber-600">
{formatCost(timePeriod === "day" ? multiResult.totals.daily_margin : multiResult.totals.monthly_margin)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-3 pt-3 border-t border-border">
<div>
<div className="text-xs text-muted-foreground">Margin Fee/Request</div>
<div className="text-sm font-mono text-amber-600 dark:text-amber-400">
{formatCost(multiResult.totals.margin_per_request)}
</div>
</Col>
</Row>
</div>
<div>
<div className="text-xs text-muted-foreground">{periodLabel} Margin Fee</div>
<div className="text-sm font-mono text-amber-600 dark:text-amber-400">{formatCost(periodMargin)}</div>
</div>
</div>
)}
</Card>
{/* Per-Model Table */}
{summaryData.length > 0 && (
<Table
columns={summaryColumns}
dataSource={summaryData}
pagination={false}
size="small"
className="border border-gray-200 rounded-lg"
expandable={{
expandedRowKeys: Array.from(expandedModels),
expandedRowRender: (record) => {
const entry = validEntries.find((e) => e.entry.id === record.id);
if (!entry?.result) return null;
return (
<div className="py-2">
<SingleModelBreakdown result={entry.result} loading={entry.loading} timePeriod={timePeriod} />
</div>
);
},
showExpandColumn: false,
}}
/>
<div className="border border-border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="h-10">Model</TableHead>
<TableHead className="h-10 text-right">Per Request</TableHead>
<TableHead className="h-10 text-right">Margin Fee</TableHead>
<TableHead className="h-10 text-right">{periodLabel}</TableHead>
<TableHead className="h-10 w-[40px]" />
</TableRow>
</TableHeader>
<TableBody>
{summaryData.map((record) => {
const isExpanded = expandedModels.has(record.id);
const periodCost = timePeriod === "day" ? record.daily_cost : record.monthly_cost;
return (
<React.Fragment key={record.id}>
<TableRow>
<TableCell className="py-2">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{record.model}</span>
{record.provider && (
<Badge variant="secondary" className="text-xs">
{record.provider}
</Badge>
)}
{record.loading && <Spinner size="sm" />}
</div>
{record.error && (
<div className="text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 px-2 py-1 rounded">
⚠️ {record.error}
</div>
)}
{record.hasZeroCost && !record.error && (
<div className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 px-2 py-1 rounded">
⚠️ No pricing data found for this model. Set base_model in config.
</div>
)}
</div>
</TableCell>
<TableCell className="py-2 text-right">
{record.error ? (
<span className="text-muted-foreground">-</span>
) : (
<span className="font-mono text-sm">{formatCost(record.cost_per_request)}</span>
)}
</TableCell>
<TableCell className="py-2 text-right">
{record.error ? (
<span className="text-muted-foreground">-</span>
) : (
<span
className={`font-mono text-sm ${
(record.margin_cost_per_request ?? 0) > 0
? "text-amber-600 dark:text-amber-400"
: "text-muted-foreground"
}`}
>
{formatCost(record.margin_cost_per_request)}
</span>
)}
</TableCell>
<TableCell className="py-2 text-right">
{record.error ? (
<span className="text-muted-foreground">-</span>
) : (
<span className="font-mono text-sm">{formatCost(periodCost)}</span>
)}
</TableCell>
<TableCell className="py-2 w-[40px]">
{record.error ? null : (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground"
onClick={() => toggleExpanded(record.id)}
aria-label={isExpanded ? "Collapse" : "Expand"}
>
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
)}
</TableCell>
</TableRow>
{isExpanded && !record.error && (
<TableRow>
<TableCell colSpan={5} className="py-2 bg-muted/50">
{(() => {
const entry = validEntries.find((e) => e.entry.id === record.id);
if (!entry?.result) return null;
return (
<SingleModelBreakdown
result={entry.result}
loading={entry.loading}
timePeriod={timePeriod}
/>
);
})()}
</TableCell>
</TableRow>
)}
</React.Fragment>
);
})}
</TableBody>
</Table>
</div>
)}
</div>
);

View file

@ -1,40 +1,10 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen, within } from "@testing-library/react";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../tests/test-utils";
import ProviderDiscountTable from "./provider_discount_table";
vi.mock("@heroicons/react/outline", () => ({
TrashIcon: function TrashIcon() { return null; },
PencilAltIcon: function PencilAltIcon() { return null; },
CheckIcon: function CheckIcon() { return null; },
XIcon: function XIcon() { return null; },
}));
vi.mock("@tremor/react", () => ({
Table: ({ children }: any) => <table>{children}</table>,
TableHead: ({ children }: any) => <thead>{children}</thead>,
TableRow: ({ children }: any) => <tr>{children}</tr>,
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
TableCell: ({ children }: any) => <td>{children}</td>,
Text: ({ children }: any) => <span>{children}</span>,
TextInput: ({ value, onValueChange, onKeyDown, placeholder, ...rest }: any) => (
<input
value={value}
onChange={(e) => onValueChange?.(e.target.value)}
onKeyDown={onKeyDown}
placeholder={placeholder}
{...rest}
/>
),
Icon: ({ icon: IconComponent, onClick }: any) => {
const name = IconComponent?.displayName ?? IconComponent?.name ?? "icon";
return <button onClick={onClick} aria-label={name} />;
},
}));
vi.mock("./provider_display_helpers", () => ({
getProviderDisplayInfo: vi.fn((providerValue: string) => ({
displayName: providerValue === "openai" ? "OpenAI" : providerValue,
@ -63,7 +33,7 @@ describe("ProviderDiscountTable", () => {
discountConfig={DEFAULT_DISCOUNT_CONFIG}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByRole("table")).toBeInTheDocument();
});
@ -74,7 +44,7 @@ describe("ProviderDiscountTable", () => {
discountConfig={DEFAULT_DISCOUNT_CONFIG}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("Provider")).toBeInTheDocument();
expect(screen.getByText("Discount Percentage")).toBeInTheDocument();
@ -87,7 +57,7 @@ describe("ProviderDiscountTable", () => {
discountConfig={DEFAULT_DISCOUNT_CONFIG}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("OpenAI")).toBeInTheDocument();
});
@ -98,23 +68,22 @@ describe("ProviderDiscountTable", () => {
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("5.0%")).toBeInTheDocument();
});
it("should show a text input when the edit icon is clicked", async () => {
it("should show a text input when the edit button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderDiscountTable
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
const pencilButton = screen.getByRole("button", { name: /PencilAltIcon/i });
await user.click(pencilButton);
await user.click(screen.getByRole("button", { name: "Edit" }));
expect(screen.getByPlaceholderText("5")).toBeInTheDocument();
});
@ -126,31 +95,31 @@ describe("ProviderDiscountTable", () => {
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
expect(screen.queryByText("5.0%")).not.toBeInTheDocument();
});
it("should call onDiscountChange with the new value when the save icon is clicked", async () => {
it("should call onDiscountChange with the new value when the save button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderDiscountTable
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
const input = screen.getByPlaceholderText("5");
await user.clear(input);
await user.type(input, "10");
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onDiscountChange).toHaveBeenCalledWith("openai", "0.1");
});
@ -162,27 +131,27 @@ describe("ProviderDiscountTable", () => {
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
await user.click(screen.getByRole("button", { name: "Save" }));
expect(screen.queryByPlaceholderText("5")).not.toBeInTheDocument();
});
it("should cancel edit mode when the cancel icon is clicked", async () => {
it("should cancel edit mode when the cancel button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderDiscountTable
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: /XIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.queryByPlaceholderText("5")).not.toBeInTheDocument();
expect(onDiscountChange).not.toHaveBeenCalled();
@ -196,26 +165,26 @@ describe("ProviderDiscountTable", () => {
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: /XIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onDiscountChange).not.toHaveBeenCalled();
});
it("should call onRemoveProvider with the provider key and display name when the trash icon is clicked", async () => {
it("should call onRemoveProvider with the provider key and display name when Remove is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderDiscountTable
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
await user.click(screen.getByRole("button", { name: "Remove" }));
expect(onRemoveProvider).toHaveBeenCalledWith("openai", "OpenAI");
});
@ -227,14 +196,14 @@ describe("ProviderDiscountTable", () => {
discountConfig={{ openai: 0.05 }}
onDiscountChange={onDiscountChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
const input = screen.getByPlaceholderText("5");
await user.clear(input);
await user.type(input, "150");
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onDiscountChange).not.toHaveBeenCalled();
});

View file

@ -5,36 +5,6 @@ import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../tests/test-utils";
import ProviderMarginTable from "./provider_margin_table";
vi.mock("@heroicons/react/outline", () => ({
TrashIcon: function TrashIcon() { return null; },
PencilAltIcon: function PencilAltIcon() { return null; },
CheckIcon: function CheckIcon() { return null; },
XIcon: function XIcon() { return null; },
}));
vi.mock("@tremor/react", () => ({
Table: ({ children }: any) => <table>{children}</table>,
TableHead: ({ children }: any) => <thead>{children}</thead>,
TableRow: ({ children }: any) => <tr>{children}</tr>,
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
TableCell: ({ children }: any) => <td>{children}</td>,
Text: ({ children }: any) => <span>{children}</span>,
TextInput: ({ value, onValueChange, placeholder, autoFocus, className }: any) => (
<input
value={value}
onChange={(e) => onValueChange?.(e.target.value)}
placeholder={placeholder}
autoFocus={autoFocus}
className={className}
/>
),
Icon: ({ icon: IconComponent, onClick }: any) => {
const name = IconComponent?.displayName ?? IconComponent?.name ?? "icon";
return <button onClick={onClick} aria-label={name} />;
},
}));
vi.mock("./provider_display_helpers", () => ({
getProviderDisplayInfo: vi.fn((providerValue: string) => {
if (providerValue === "openai") return { displayName: "OpenAI", logo: "", enumKey: "OpenAI" };
@ -58,7 +28,7 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByRole("table")).toBeInTheDocument();
});
@ -69,7 +39,7 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("Provider")).toBeInTheDocument();
expect(screen.getByText("Margin")).toBeInTheDocument();
@ -82,7 +52,7 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("OpenAI")).toBeInTheDocument();
});
@ -93,7 +63,7 @@ describe("ProviderMarginTable", () => {
marginConfig={{ global: 0.05 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("Global (All Providers)")).toBeInTheDocument();
});
@ -104,7 +74,7 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("10.0%")).toBeInTheDocument();
});
@ -115,7 +85,7 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: { fixed_amount: 0.001 } }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText("$0.001000")).toBeInTheDocument();
});
@ -126,22 +96,22 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: { percentage: 0.1, fixed_amount: 0.001 } }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
expect(screen.getByText(/10\.0%.*\$0\.001000/)).toBeInTheDocument();
});
it("should show edit inputs when the pencil icon is clicked", async () => {
it("should show edit inputs when the edit button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderMarginTable
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
expect(screen.getByPlaceholderText("10")).toBeInTheDocument();
expect(screen.getByPlaceholderText("0.001")).toBeInTheDocument();
@ -154,48 +124,48 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
const percentInput = screen.getByPlaceholderText("10");
await user.clear(percentInput);
await user.type(percentInput, "20");
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onMarginChange).toHaveBeenCalledWith("openai", 0.2);
});
it("should cancel edit mode without calling onMarginChange when X is clicked", async () => {
it("should cancel edit mode without calling onMarginChange when Cancel is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderMarginTable
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: /XIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onMarginChange).not.toHaveBeenCalled();
expect(screen.queryByPlaceholderText("10")).not.toBeInTheDocument();
});
it("should call onRemoveProvider with provider key and display name when trash is clicked", async () => {
it("should call onRemoveProvider with provider key and display name when remove is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<ProviderMarginTable
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
await user.click(screen.getByRole("button", { name: "Remove" }));
expect(onRemoveProvider).toHaveBeenCalledWith("openai", "OpenAI");
});
@ -207,10 +177,10 @@ describe("ProviderMarginTable", () => {
marginConfig={{ global: 0.05 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
await user.click(screen.getByRole("button", { name: "Remove" }));
expect(onRemoveProvider).toHaveBeenCalledWith("global", "Global");
});
@ -223,10 +193,10 @@ describe("ProviderMarginTable", () => {
marginConfig={{ openai: 0.1 }}
onMarginChange={onMarginChange}
onRemoveProvider={onRemoveProvider}
/>
/>,
);
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
await user.click(screen.getByRole("button", { name: "Edit" }));
const percentInput = screen.getByPlaceholderText("10");
await user.clear(percentInput);
@ -235,7 +205,7 @@ describe("ProviderMarginTable", () => {
const fixedInput = screen.getByPlaceholderText("0.001");
await user.type(fixedInput, "0.002");
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onMarginChange).toHaveBeenCalledWith("openai", {
percentage: 0.05,