[UI] Add view for estimating costs across requests (#18645)

* add estimate_cost endpoint

* TestCostEstimateEndpoint

* fix estimate_cost

* add /cost/estimate to spend tracking routes

* fix code QA checks

* fixes endpoint

* v0 cost estimator

* v0 cost estimator

* formatNumberWithCommas

* fix admin view

* docs

* docs fix + export PDF/CSV

* fixes for export
This commit is contained in:
Ishaan Jaff
2026-01-05 19:19:58 +05:30
committed by GitHub
parent d0a26dd4bc
commit bf1c5bef59
11 changed files with 1111 additions and 112 deletions
@@ -0,0 +1,142 @@
# Pricing Calculator (Cost Estimation)
Estimate LLM costs based on expected token usage and request volume. This tool helps developers and platform teams forecast spending before deploying models to production.
## When to Use This Feature
Use the Pricing Calculator to:
- **Budget planning** - Estimate monthly costs before committing to a model
- **Model comparison** - Compare costs across different models for your use case
- **Capacity planning** - Understand cost implications of scaling request volume
- **Cost optimization** - Identify the most cost-effective model for your token requirements
## Using the Pricing Calculator
This walkthrough shows how to estimate LLM costs using the Pricing Calculator in the LiteLLM UI.
### Step 1: Navigate to Settings
From the LiteLLM dashboard, click on **Settings** in the left sidebar.
![Click Settings](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/183c437e-bda9-48b4-ab8f-95f023ba1146/ascreenshot_a1013487f545484194a9a4929eef4c49_text_export.jpeg)
### Step 2: Open Cost Tracking
Click on **Cost Tracking** to access the cost configuration options.
![Click Cost Tracking](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/05c92350-cbae-42ed-935b-e96a26003de8/ascreenshot_cc85f175a6664fc5be8dfdcc1759b442_text_export.jpeg)
### Step 3: Open Pricing Calculator
Click on **Pricing Calculator** to expand the calculator panel. This section allows you to estimate LLM costs based on expected token usage and request volume.
![Click Pricing Calculator](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/31ab5547-fa7d-4abd-b41a-7b4bbc0401f7/ascreenshot_f7f8b098ceba4b5199e5cbc60dddfd0a_text_export.jpeg)
### Step 4: Select a Model
Click the **Model** dropdown to select the model you want to estimate costs for.
![Click Model field](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/a6c236ce-3154-42a8-9701-120e3f7a017b/ascreenshot_635c61b832594e809f8ab79b5b3f32e1_text_export.jpeg)
Choose a model from the list. The models shown are the ones configured on your LiteLLM proxy.
![Select model](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/96c4ebc4-1b88-4dea-b3b2-ea32fde36d9e/ascreenshot_7c2920f05a984ebbb530a8a85e669537_text_export.jpeg)
### Step 5: Configure Token Counts
Enter the expected **Input Tokens (per request)** - this is the average number of tokens in your prompts.
![Click Input Tokens field](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/d0b5ad8a-56e4-4f73-ac66-e1d728c81dc5/ascreenshot_42502082d6204a3891e0a2c3e89a1e38_text_export.jpeg)
Enter the expected **Output Tokens (per request)** - this is the average number of tokens in model responses.
![Click Output Tokens field](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/d7481177-c63c-47f5-9316-1e87695f67f9/ascreenshot_8718cac4c0d14a82ab9f2b71795250c2_text_export.jpeg)
### Step 6: Set Request Volume
Enter your expected request volume. You can specify **Requests per Day** and/or **Requests per Month**.
![Click Requests per Month field](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/42270e11-93f1-41dc-b9c7-3bb6971ced31/ascreenshot_79f2ea9937b34e48ab1ff832ce7f7cb7_text_export.jpeg)
For example, enter `10000000` for 10 million requests per month.
![Enter request volume](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/5e6c4338-ff87-44dd-9059-7577217fa3c8/ascreenshot_15c36610dc914536ac9446470eb39f05_text_export.jpeg)
### Step 7: View Cost Estimates
The calculator automatically updates as you change values. View the cost breakdown including:
- **Per-Request Cost** - Total cost, input cost, output cost, and margin/fee per request
- **Daily Costs** - Aggregated costs if you specified requests per day
- **Monthly Costs** - Aggregated costs if you specified requests per month
![View cost estimates](https://colony-recorder.s3.amazonaws.com/files/2026-01-05/4436cd11-df58-47cb-9742-c0d08865a61c/ascreenshot_f961298a4231464ea841bc4d184f731e_text_export.jpeg)
### Step 8: Export the Report
Click the **Export** button to download your cost estimate. You can export as:
- **PDF** - Opens a print dialog to save as PDF (great for sharing with stakeholders)
- **CSV** - Downloads a spreadsheet-compatible file for further analysis
## Cost Breakdown Details
The Pricing Calculator shows:
| Field | Description |
|-------|-------------|
| **Total Cost** | Complete cost including any configured margins |
| **Input Cost** | Cost for input/prompt tokens |
| **Output Cost** | Cost for output/completion tokens |
| **Margin/Fee** | Any configured [provider margins](/docs/proxy/provider_margins) |
| **Token Pricing** | Per-token rates (shown as $/1M tokens) |
## API Endpoint
You can also estimate costs programmatically using the `/cost/estimate` endpoint:
```bash
curl -X POST "http://localhost:4000/cost/estimate" \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"input_tokens": 1000,
"output_tokens": 500,
"num_requests_per_day": 1000,
"num_requests_per_month": 30000
}'
```
**Response:**
```json
{
"model": "gpt-4",
"input_tokens": 1000,
"output_tokens": 500,
"num_requests_per_day": 1000,
"num_requests_per_month": 30000,
"cost_per_request": 0.045,
"input_cost_per_request": 0.03,
"output_cost_per_request": 0.015,
"margin_cost_per_request": 0.0,
"daily_cost": 45.0,
"daily_input_cost": 30.0,
"daily_output_cost": 15.0,
"daily_margin_cost": 0.0,
"monthly_cost": 1350.0,
"monthly_input_cost": 900.0,
"monthly_output_cost": 450.0,
"monthly_margin_cost": 0.0,
"input_cost_per_token": 3e-05,
"output_cost_per_token": 6e-05,
"provider": "openai"
}
```
## Related Features
- [Provider Margins](/docs/proxy/provider_margins) - Add fees or margins to LLM costs
- [Provider Discounts](/docs/proxy/provider_discounts) - Apply discounts to provider costs
- [Cost Tracking](/docs/proxy/cost_tracking) - Track and monitor LLM spend
+1
View File
@@ -390,6 +390,7 @@ const sidebars = {
items: [
"proxy/cost_tracking",
"proxy/custom_pricing",
"proxy/pricing_calculator",
"proxy/provider_margins",
"proxy/provider_discounts",
"proxy/sync_models_github",
@@ -6,11 +6,13 @@ 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 { ExclamationCircleOutlined } from "@ant-design/icons";
import { DocsMenu } from "../HelpLink";
import HowItWorks from "./how_it_works";
import { useDiscountConfig } from "./use_discount_config";
import { useMarginConfig } from "./use_margin_config";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
const DOCS_LINKS = [
{ label: "Custom pricing for models", href: "https://docs.litellm.ai/docs/proxy/custom_pricing" },
@@ -31,9 +33,12 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
const [marginType, setMarginType] = useState<"percentage" | "fixed">("percentage");
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 isProxyAdmin = userRole === "proxy_admin" || userRole === "Admin";
// Use custom hooks for discount and margin config
const {
@@ -57,6 +62,17 @@ 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);
setModels(modelGroups.map((m: ModelGroup) => m.model_group));
} catch (error) {
console.error("Error fetching models:", error);
}
};
loadModels();
}
}, [accessToken, fetchDiscountConfig, fetchMarginConfig]);
@@ -152,129 +168,153 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
{/* Main Content Card with Accordions */}
<div className="bg-white rounded-lg shadow w-full max-w-full space-y-4">
{/* Accordion 1: Provider Discounts */}
<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="p-6">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsModalVisible(true)}
>
+ Add Provider Discount
</Button>
</div>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</div>
) : Object.keys(discountConfig).length > 0 ? (
<ProviderDiscountTable
discountConfig={discountConfig}
onDiscountChange={handleDiscountChange}
onRemoveProvider={handleRemoveProvider}
/>
) : (
<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"
{/* 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="p-6">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsModalVisible(true)}
>
<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">
Click &quot;Add Provider Discount&quot; to get started
</Text>
+ Add Provider Discount
</Button>
</div>
)}
</div>
</TabPanel>
<TabPanel>
<div className="px-6 pb-4">
<HowItWorks />
</div>
</TabPanel>
</TabPanels>
</TabGroup>
</AccordionBody>
</Accordion>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</div>
) : Object.keys(discountConfig).length > 0 ? (
<ProviderDiscountTable
discountConfig={discountConfig}
onDiscountChange={handleDiscountChange}
onRemoveProvider={handleRemoveProvider}
/>
) : (
<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">
Click &quot;Add Provider Discount&quot; to get started
</Text>
</div>
)}
</div>
</TabPanel>
<TabPanel>
<div className="px-6 pb-4">
<HowItWorks />
</div>
</TabPanel>
</TabPanels>
</TabGroup>
</AccordionBody>
</Accordion>
)}
{/* Accordion 2: Fee/Price Margin */}
<Accordion>
{/* 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>
</div>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</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>
)}
{/* 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">Fee/Price Margin</Text>
<Text className="text-lg font-semibold text-gray-900">Pricing Calculator</Text>
<Text className="text-sm text-gray-500 mt-1">
Add fees or margins to LLM costs for internal billing and cost recovery
Estimate LLM costs based on expected token usage and request volume
</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>
</div>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</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>
)}
<PricingCalculator
accessToken={accessToken}
models={models}
/>
</div>
</AccordionBody>
</Accordion>
@@ -0,0 +1,203 @@
import React from "react";
import { Text } from "@tremor/react";
import { Card, Statistic, Row, Col, Divider, Spin } from "antd";
import { DollarOutlined, LoadingOutlined } from "@ant-design/icons";
import { CostEstimateResponse } from "../types";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import ExportDropdown from "./export_dropdown";
interface CostResultsProps {
result: CostEstimateResponse | null;
loading: boolean;
}
const formatCost = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
if (value === 0) return "$0";
if (value < 0.0001) return `$${value.toExponential(2)}`;
if (value < 1) return `$${value.toFixed(4)}`;
return `$${formatNumberWithCommas(value, 2, true)}`;
};
const formatRequests = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
return formatNumberWithCommas(value, 0, true);
};
const CostResults: React.FC<CostResultsProps> = ({ result, loading }) => {
if (!result && !loading) {
return (
<div className="py-8 text-center border border-dashed border-gray-300 rounded-lg">
<Text className="text-gray-500">
Select a model to see cost estimates
</Text>
</div>
);
}
if (loading && !result) {
return (
<div className="py-8 text-center">
<Spin indicator={<LoadingOutlined spin />} />
<Text className="text-gray-500 block mt-2">Calculating costs...</Text>
</div>
);
}
if (!result) return null;
return (
<div className="space-y-4">
<Divider />
<div className="mb-4 flex items-center justify-between">
<div>
<Text className="text-lg font-semibold text-gray-900">Cost Estimate</Text>
<Text className="text-sm text-gray-500 block mt-1">
Model: {result.model} {result.provider && `(${result.provider})`}
</Text>
</div>
<div className="flex items-center gap-2">
{loading && <Spin indicator={<LoadingOutlined spin />} size="small" />}
<ExportDropdown result={result} />
</div>
</div>
<Card size="small" title="Per-Request Cost Breakdown">
<Row gutter={16}>
<Col span={6}>
<Statistic
title="Total Cost"
value={formatCost(result.cost_per_request)}
valueStyle={{ color: "#1890ff", fontSize: "18px" }}
prefix={<DollarOutlined />}
/>
</Col>
<Col span={6}>
<Statistic
title="Input Cost"
value={formatCost(result.input_cost_per_request)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Output Cost"
value={formatCost(result.output_cost_per_request)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Margin/Fee"
value={formatCost(result.margin_cost_per_request)}
valueStyle={{
fontSize: "16px",
color: result.margin_cost_per_request > 0 ? "#faad14" : undefined,
}}
/>
</Col>
</Row>
</Card>
{result.daily_cost !== null && (
<Card
size="small"
title={`Daily Costs (${formatRequests(result.num_requests_per_day)} requests/day)`}
>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="Total Daily"
value={formatCost(result.daily_cost)}
valueStyle={{ color: "#52c41a", fontSize: "18px" }}
prefix={<DollarOutlined />}
/>
</Col>
<Col span={6}>
<Statistic
title="Input Cost"
value={formatCost(result.daily_input_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Output Cost"
value={formatCost(result.daily_output_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Margin/Fee"
value={formatCost(result.daily_margin_cost)}
valueStyle={{
fontSize: "16px",
color: (result.daily_margin_cost ?? 0) > 0 ? "#faad14" : undefined,
}}
/>
</Col>
</Row>
</Card>
)}
{result.monthly_cost !== null && (
<Card
size="small"
title={`Monthly Costs (${formatRequests(result.num_requests_per_month)} requests/month)`}
>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="Total Monthly"
value={formatCost(result.monthly_cost)}
valueStyle={{ color: "#722ed1", fontSize: "18px" }}
prefix={<DollarOutlined />}
/>
</Col>
<Col span={6}>
<Statistic
title="Input Cost"
value={formatCost(result.monthly_input_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Output Cost"
value={formatCost(result.monthly_output_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Margin/Fee"
value={formatCost(result.monthly_margin_cost)}
valueStyle={{
fontSize: "16px",
color: (result.monthly_margin_cost ?? 0) > 0 ? "#faad14" : undefined,
}}
/>
</Col>
</Row>
</Card>
)}
{(result.input_cost_per_token || result.output_cost_per_token) && (
<div className="text-sm text-gray-500 mt-4">
<Text className="font-medium">Token Pricing: </Text>
{result.input_cost_per_token && (
<span>Input: ${formatNumberWithCommas(result.input_cost_per_token * 1_000_000, 2)}/1M tokens</span>
)}
{result.input_cost_per_token && result.output_cost_per_token && " | "}
{result.output_cost_per_token && (
<span>Output: ${formatNumberWithCommas(result.output_cost_per_token * 1_000_000, 2)}/1M tokens</span>
)}
</div>
)}
</div>
);
};
export default CostResults;
@@ -0,0 +1,71 @@
import React, { useState, useRef, useEffect } from "react";
import { Button } from "@tremor/react";
import { DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons";
import { CostEstimateResponse } from "../types";
import { exportToPDF, exportToCSV } from "./export_utils";
interface ExportDropdownProps {
result: CostEstimateResponse;
}
const ExportDropdown: React.FC<ExportDropdownProps> = ({ result }) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
return (
<div className="relative inline-block" ref={menuRef}>
<Button
size="xs"
variant="secondary"
icon={DownloadOutlined}
onClick={() => setIsOpen(!isOpen)}
>
Export
</Button>
{isOpen && (
<div className="absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
exportToPDF(result);
setIsOpen(false);
}}
>
<FilePdfOutlined className="mr-3 text-red-500" />
Export as PDF
</button>
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
exportToCSV(result);
setIsOpen(false);
}}
>
<FileExcelOutlined className="mr-3 text-green-600" />
Export as CSV
</button>
</div>
)}
</div>
);
};
export default ExportDropdown;
@@ -0,0 +1,276 @@
import { CostEstimateResponse } from "../types";
import { formatNumberWithCommas } from "@/utils/dataUtils";
const formatCostForExport = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
if (value === 0) return "$0.00";
if (value < 0.01) return `$${value.toFixed(6)}`;
if (value < 1) return `$${value.toFixed(4)}`;
return `$${formatNumberWithCommas(value, 2)}`;
};
const formatRequestsForExport = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
return formatNumberWithCommas(value, 0);
};
export const exportToPDF = (result: CostEstimateResponse): void => {
const printWindow = window.open("", "_blank");
if (!printWindow) {
alert("Please allow popups to export PDF");
return;
}
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Cost Estimate Report - ${result.model}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 40px;
max-width: 800px;
margin: 0 auto;
color: #333;
}
h1 {
color: #1a1a1a;
border-bottom: 2px solid #1890ff;
padding-bottom: 10px;
margin-bottom: 30px;
}
h2 {
color: #444;
margin-top: 30px;
margin-bottom: 15px;
}
.meta {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 30px;
}
.meta p {
margin: 5px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
}
.cost-value {
font-family: monospace;
font-size: 14px;
}
.total-row {
font-weight: bold;
background: #e6f7ff;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 12px;
color: #666;
}
@media print {
body { padding: 20px; }
}
</style>
</head>
<body>
<h1>LLM Cost Estimate Report</h1>
<div class="meta">
<p><strong>Model:</strong> ${result.model}</p>
${result.provider ? `<p><strong>Provider:</strong> ${result.provider}</p>` : ""}
<p><strong>Input Tokens per Request:</strong> ${formatRequestsForExport(result.input_tokens)}</p>
<p><strong>Output Tokens per Request:</strong> ${formatRequestsForExport(result.output_tokens)}</p>
${result.num_requests_per_day ? `<p><strong>Requests per Day:</strong> ${formatRequestsForExport(result.num_requests_per_day)}</p>` : ""}
${result.num_requests_per_month ? `<p><strong>Requests per Month:</strong> ${formatRequestsForExport(result.num_requests_per_month)}</p>` : ""}
</div>
<h2>Per-Request Cost Breakdown</h2>
<table>
<tr>
<th>Cost Type</th>
<th>Amount</th>
</tr>
<tr>
<td>Input Cost</td>
<td class="cost-value">${formatCostForExport(result.input_cost_per_request)}</td>
</tr>
<tr>
<td>Output Cost</td>
<td class="cost-value">${formatCostForExport(result.output_cost_per_request)}</td>
</tr>
<tr>
<td>Margin/Fee</td>
<td class="cost-value">${formatCostForExport(result.margin_cost_per_request)}</td>
</tr>
<tr class="total-row">
<td>Total per Request</td>
<td class="cost-value">${formatCostForExport(result.cost_per_request)}</td>
</tr>
</table>
${result.daily_cost !== null ? `
<h2>Daily Cost Estimate (${formatRequestsForExport(result.num_requests_per_day)} requests/day)</h2>
<table>
<tr>
<th>Cost Type</th>
<th>Amount</th>
</tr>
<tr>
<td>Input Cost</td>
<td class="cost-value">${formatCostForExport(result.daily_input_cost)}</td>
</tr>
<tr>
<td>Output Cost</td>
<td class="cost-value">${formatCostForExport(result.daily_output_cost)}</td>
</tr>
<tr>
<td>Margin/Fee</td>
<td class="cost-value">${formatCostForExport(result.daily_margin_cost)}</td>
</tr>
<tr class="total-row">
<td>Total Daily</td>
<td class="cost-value">${formatCostForExport(result.daily_cost)}</td>
</tr>
</table>
` : ""}
${result.monthly_cost !== null ? `
<h2>Monthly Cost Estimate (${formatRequestsForExport(result.num_requests_per_month)} requests/month)</h2>
<table>
<tr>
<th>Cost Type</th>
<th>Amount</th>
</tr>
<tr>
<td>Input Cost</td>
<td class="cost-value">${formatCostForExport(result.monthly_input_cost)}</td>
</tr>
<tr>
<td>Output Cost</td>
<td class="cost-value">${formatCostForExport(result.monthly_output_cost)}</td>
</tr>
<tr>
<td>Margin/Fee</td>
<td class="cost-value">${formatCostForExport(result.monthly_margin_cost)}</td>
</tr>
<tr class="total-row">
<td>Total Monthly</td>
<td class="cost-value">${formatCostForExport(result.monthly_cost)}</td>
</tr>
</table>
` : ""}
${result.input_cost_per_token || result.output_cost_per_token ? `
<h2>Token Pricing</h2>
<table>
<tr>
<th>Token Type</th>
<th>Price per 1M Tokens</th>
</tr>
${result.input_cost_per_token ? `
<tr>
<td>Input Tokens</td>
<td class="cost-value">$${(result.input_cost_per_token * 1000000).toFixed(2)}</td>
</tr>
` : ""}
${result.output_cost_per_token ? `
<tr>
<td>Output Tokens</td>
<td class="cost-value">$${(result.output_cost_per_token * 1000000).toFixed(2)}</td>
</tr>
` : ""}
</table>
` : ""}
<div class="footer">
<p>Generated by LiteLLM Pricing Calculator on ${new Date().toLocaleString()}</p>
</div>
</body>
</html>
`;
printWindow.document.write(html);
printWindow.document.close();
printWindow.onload = () => {
printWindow.print();
};
};
export const exportToCSV = (result: CostEstimateResponse): void => {
const rows = [
["LLM Cost Estimate Report"],
[""],
["Configuration"],
["Model", result.model],
["Provider", result.provider || "-"],
["Input Tokens per Request", result.input_tokens.toString()],
["Output Tokens per Request", result.output_tokens.toString()],
["Requests per Day", result.num_requests_per_day?.toString() || "-"],
["Requests per Month", result.num_requests_per_month?.toString() || "-"],
[""],
["Per-Request Costs"],
["Input Cost", result.input_cost_per_request.toString()],
["Output Cost", result.output_cost_per_request.toString()],
["Margin/Fee", result.margin_cost_per_request.toString()],
["Total per Request", result.cost_per_request.toString()],
];
if (result.daily_cost !== null) {
rows.push(
[""],
["Daily Costs"],
["Daily Input Cost", result.daily_input_cost?.toString() || "-"],
["Daily Output Cost", result.daily_output_cost?.toString() || "-"],
["Daily Margin/Fee", result.daily_margin_cost?.toString() || "-"],
["Total Daily", result.daily_cost.toString()]
);
}
if (result.monthly_cost !== null) {
rows.push(
[""],
["Monthly Costs"],
["Monthly Input Cost", result.monthly_input_cost?.toString() || "-"],
["Monthly Output Cost", result.monthly_output_cost?.toString() || "-"],
["Monthly Margin/Fee", result.monthly_margin_cost?.toString() || "-"],
["Total Monthly", result.monthly_cost.toString()]
);
}
if (result.input_cost_per_token || result.output_cost_per_token) {
rows.push(
[""],
["Token Pricing (per 1M tokens)"],
["Input Token Price", result.input_cost_per_token ? `$${(result.input_cost_per_token * 1000000).toFixed(2)}` : "-"],
["Output Token Price", result.output_cost_per_token ? `$${(result.output_cost_per_token * 1000000).toFixed(2)}` : "-"]
);
}
const csv = rows.map(row => row.join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `cost_estimate_${result.model.replace(/\//g, "_")}_${new Date().toISOString().split("T")[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
@@ -0,0 +1,31 @@
import React, { useCallback } from "react";
import PricingForm from "./pricing_form";
import CostResults from "./cost_results";
import { useCostEstimate } from "./use_cost_estimate";
import { PricingCalculatorProps, PricingFormValues } from "./types";
const PricingCalculator: React.FC<PricingCalculatorProps> = ({
accessToken,
models,
}) => {
const { loading, result, debouncedFetch } = useCostEstimate(accessToken);
const handleValuesChange = useCallback(
(_changedValues: Partial<PricingFormValues>, allValues: PricingFormValues) => {
if (allValues.model) {
debouncedFetch(allValues);
}
},
[debouncedFetch]
);
return (
<div className="space-y-6">
<PricingForm models={models} onValuesChange={handleValuesChange} />
<CostResults result={result} loading={loading} />
</div>
);
};
export default PricingCalculator;
@@ -0,0 +1,104 @@
import React from "react";
import { Form, InputNumber, Select, Row, Col } from "antd";
import { PricingFormValues } from "./types";
interface PricingFormProps {
models: string[];
onValuesChange: (changedValues: Partial<PricingFormValues>, allValues: PricingFormValues) => void;
}
const PricingForm: React.FC<PricingFormProps> = ({ models, onValuesChange }) => {
return (
<Form
layout="vertical"
onValuesChange={onValuesChange}
initialValues={{
input_tokens: 1000,
output_tokens: 500,
}}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="model"
label="Model"
rules={[{ required: true, message: "Please select a model" }]}
>
<Select
showSearch
placeholder="Select a model"
optionFilterProp="label"
filterOption={(input, option) =>
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={models.map((model) => ({
value: model,
label: model,
}))}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="input_tokens"
label="Input Tokens (per request)"
rules={[{ required: true, message: "Required" }]}
>
<InputNumber
min={0}
style={{ width: "100%" }}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="output_tokens"
label="Output Tokens (per request)"
rules={[{ required: true, message: "Required" }]}
>
<InputNumber
min={0}
style={{ width: "100%" }}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="num_requests_per_day"
label="Requests per Day"
tooltip="Optional: Enter expected daily request volume"
>
<InputNumber
min={0}
style={{ width: "100%" }}
placeholder="e.g., 1000"
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="num_requests_per_month"
label="Requests per Month"
tooltip="Optional: Enter expected monthly request volume"
>
<InputNumber
min={0}
style={{ width: "100%" }}
placeholder="e.g., 30000"
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
</Row>
</Form>
);
};
export default PricingForm;
@@ -0,0 +1,13 @@
export interface PricingCalculatorProps {
accessToken: string | null;
models: string[];
}
export interface PricingFormValues {
model: string;
input_tokens: number;
output_tokens: number;
num_requests_per_day?: number;
num_requests_per_month?: number;
}
@@ -0,0 +1,87 @@
import { useState, useCallback, useRef, useEffect } from "react";
import { getProxyBaseUrl } from "@/components/networking";
import NotificationsManager from "../../molecules/notifications_manager";
import { CostEstimateRequest, CostEstimateResponse } from "../types";
import { PricingFormValues } from "./types";
const DEBOUNCE_MS = 500;
export function useCostEstimate(accessToken: string | null) {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<CostEstimateResponse | null>(null);
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const fetchEstimate = useCallback(
async (values: PricingFormValues) => {
if (!accessToken || !values.model) {
setResult(null);
return;
}
setLoading(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/cost/estimate`
: "/cost/estimate";
const requestBody: CostEstimateRequest = {
model: values.model,
input_tokens: values.input_tokens || 0,
output_tokens: values.output_tokens || 0,
num_requests_per_day: values.num_requests_per_day || null,
num_requests_per_month: values.num_requests_per_month || null,
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (response.ok) {
const data: CostEstimateResponse = await response.json();
setResult(data);
} else {
const errorData = await response.json();
const errorMessage =
errorData.detail?.error || errorData.detail || "Failed to estimate cost";
NotificationsManager.fromBackend(errorMessage);
setResult(null);
}
} catch (error) {
console.error("Error estimating cost:", error);
setResult(null);
} finally {
setLoading(false);
}
},
[accessToken]
);
const debouncedFetch = useCallback(
(values: PricingFormValues) => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
fetchEstimate(values);
}, DEBOUNCE_MS);
},
[fetchEstimate]
);
useEffect(() => {
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
return { loading, result, debouncedFetch };
}
@@ -20,3 +20,34 @@ export interface CostMarginResponse {
values: MarginConfig;
}
export interface CostEstimateRequest {
model: string;
input_tokens: number;
output_tokens: number;
num_requests_per_day?: number | null;
num_requests_per_month?: number | null;
}
export interface CostEstimateResponse {
model: string;
input_tokens: number;
output_tokens: number;
num_requests_per_day: number | null;
num_requests_per_month: number | null;
cost_per_request: number;
input_cost_per_request: number;
output_cost_per_request: number;
margin_cost_per_request: number;
daily_cost: number | null;
daily_input_cost: number | null;
daily_output_cost: number | null;
daily_margin_cost: number | null;
monthly_cost: number | null;
monthly_input_cost: number | null;
monthly_output_cost: number | null;
monthly_margin_cost: number | null;
input_cost_per_token: number | null;
output_cost_per_token: number | null;
provider: string | null;
}