mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 18:24:49 +00:00
[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:
@@ -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.
|
||||
|
||||

|
||||
|
||||
### Step 2: Open Cost Tracking
|
||||
|
||||
Click on **Cost Tracking** to access the cost configuration options.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### Step 4: Select a Model
|
||||
|
||||
Click the **Model** dropdown to select the model you want to estimate costs for.
|
||||
|
||||

|
||||
|
||||
Choose a model from the list. The models shown are the ones configured on your LiteLLM proxy.
|
||||
|
||||

|
||||
|
||||
### Step 5: Configure Token Counts
|
||||
|
||||
Enter the expected **Input Tokens (per request)** - this is the average number of tokens in your prompts.
|
||||
|
||||

|
||||
|
||||
Enter the expected **Output Tokens (per request)** - this is the average number of tokens in model responses.
|
||||
|
||||

|
||||
|
||||
### Step 6: Set Request Volume
|
||||
|
||||
Enter your expected request volume. You can specify **Requests per Day** and/or **Requests per Month**.
|
||||
|
||||

|
||||
|
||||
For example, enter `10000000` for 10 million requests per month.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+152
-112
@@ -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 "Add Provider Discount" 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 "Add Provider Discount" 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 "Add Provider Margin" 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 "Add Provider Margin" to get started
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<PricingCalculator
|
||||
accessToken={accessToken}
|
||||
models={models}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
+203
@@ -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;
|
||||
+71
@@ -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;
|
||||
|
||||
+276
@@ -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;
|
||||
|
||||
+104
@@ -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;
|
||||
}
|
||||
|
||||
+87
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user