mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
docs fix + export PDF/CSV
This commit is contained in:
parent
e9463e4572
commit
f5859c54c9
3 changed files with 350 additions and 4 deletions
|
|
@ -72,6 +72,13 @@ The calculator automatically updates as you change values. View the cost breakdo
|
|||
|
||||

|
||||
|
||||
### 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:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from "react";
|
||||
import { Text } from "@tremor/react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Text, Button } from "@tremor/react";
|
||||
import { Card, Statistic, Row, Col, Divider, Spin } from "antd";
|
||||
import { DollarOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { DollarOutlined, LoadingOutlined, DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons";
|
||||
import { CostEstimateResponse } from "../types";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { exportToPDF, exportToCSV } from "./export_utils";
|
||||
|
||||
interface CostResultsProps {
|
||||
result: CostEstimateResponse | null;
|
||||
|
|
@ -23,6 +24,65 @@ const formatRequests = (value: number | null | undefined): string => {
|
|||
return formatNumberWithCommas(value, 0, true);
|
||||
};
|
||||
|
||||
const ExportDropdown: React.FC<{ result: CostEstimateResponse }> = ({ 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>
|
||||
);
|
||||
};
|
||||
|
||||
const CostResults: React.FC<CostResultsProps> = ({ result, loading }) => {
|
||||
if (!result && !loading) {
|
||||
return (
|
||||
|
|
@ -56,7 +116,10 @@ const CostResults: React.FC<CostResultsProps> = ({ result, loading }) => {
|
|||
Model: {result.model} {result.provider && `(${result.provider})`}
|
||||
</Text>
|
||||
</div>
|
||||
{loading && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
Loading…
Add table
Reference in a new issue