mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
v0 cost estimator
This commit is contained in:
parent
59ec4b53b2
commit
5123ec0815
6 changed files with 463 additions and 0 deletions
|
|
@ -0,0 +1,197 @@
|
|||
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";
|
||||
|
||||
interface CostResultsProps {
|
||||
result: CostEstimateResponse | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const formatCurrency = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
if (value < 0.01) return `$${value.toFixed(6)}`;
|
||||
return `$${value.toFixed(4)}`;
|
||||
};
|
||||
|
||||
const formatLargeCurrency = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
return `$${value.toFixed(2)}`;
|
||||
};
|
||||
|
||||
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>
|
||||
{loading && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
</div>
|
||||
|
||||
<Card size="small" title="Per-Request Cost Breakdown">
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Total Cost"
|
||||
value={formatCurrency(result.cost_per_request)}
|
||||
valueStyle={{ color: "#1890ff", fontSize: "18px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Input Cost"
|
||||
value={formatCurrency(result.input_cost_per_request)}
|
||||
valueStyle={{ fontSize: "16px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Output Cost"
|
||||
value={formatCurrency(result.output_cost_per_request)}
|
||||
valueStyle={{ fontSize: "16px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Margin/Fee"
|
||||
value={formatCurrency(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 (${result.num_requests_per_day?.toLocaleString()} requests/day)`}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Total Daily"
|
||||
value={formatLargeCurrency(result.daily_cost)}
|
||||
valueStyle={{ color: "#52c41a", fontSize: "18px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Input Cost"
|
||||
value={formatLargeCurrency(result.daily_input_cost)}
|
||||
valueStyle={{ fontSize: "16px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Output Cost"
|
||||
value={formatLargeCurrency(result.daily_output_cost)}
|
||||
valueStyle={{ fontSize: "16px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Margin/Fee"
|
||||
value={formatLargeCurrency(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 (${result.num_requests_per_month?.toLocaleString()} requests/month)`}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Total Monthly"
|
||||
value={formatLargeCurrency(result.monthly_cost)}
|
||||
valueStyle={{ color: "#722ed1", fontSize: "18px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Input Cost"
|
||||
value={formatLargeCurrency(result.monthly_input_cost)}
|
||||
valueStyle={{ fontSize: "16px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Output Cost"
|
||||
value={formatLargeCurrency(result.monthly_output_cost)}
|
||||
valueStyle={{ fontSize: "16px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Margin/Fee"
|
||||
value={formatLargeCurrency(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: ${(result.input_cost_per_token * 1000000).toFixed(2)}/1M tokens</span>
|
||||
)}
|
||||
{result.input_cost_per_token && result.output_cost_per_token && " | "}
|
||||
{result.output_cost_per_token && (
|
||||
<span>Output: ${(result.output_cost_per_token * 1000000).toFixed(2)}/1M tokens</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CostResults;
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue