mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
v2 of report
This commit is contained in:
parent
0e4448a731
commit
69aefb8a94
6 changed files with 326 additions and 337 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
import { Button, Text } from "@tremor/react";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Table, Select, InputNumber, Button } from "antd";
|
||||
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { PricingCalculatorProps, ModelEntry } from "./types";
|
||||
import ModelEntryRow from "./model_entry_row";
|
||||
import MultiCostResults from "./multi_cost_results";
|
||||
import { useMultiCostEstimate } from "./use_multi_cost_estimate";
|
||||
|
||||
|
|
@ -53,38 +52,135 @@ const PricingCalculator: React.FC<PricingCalculatorProps> = ({
|
|||
[removeEntry]
|
||||
);
|
||||
|
||||
// Note: Fetching is triggered by handleEntryChange when model is selected
|
||||
|
||||
const multiModelResult = getMultiModelResult(entries);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "Model",
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
width: "30%",
|
||||
render: (_: string, record: ModelEntry) => (
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Select a model"
|
||||
value={record.model || undefined}
|
||||
onChange={(value) => handleEntryChange(record.id, "model", value)}
|
||||
optionFilterProp="label"
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={models.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
}))}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Input Tokens",
|
||||
dataIndex: "input_tokens",
|
||||
key: "input_tokens",
|
||||
width: "15%",
|
||||
render: (_: number, record: ModelEntry) => (
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={record.input_tokens}
|
||||
onChange={(value) => handleEntryChange(record.id, "input_tokens", value ?? 0)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Output Tokens",
|
||||
dataIndex: "output_tokens",
|
||||
key: "output_tokens",
|
||||
width: "15%",
|
||||
render: (_: number, record: ModelEntry) => (
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={record.output_tokens}
|
||||
onChange={(value) => handleEntryChange(record.id, "output_tokens", value ?? 0)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Requests/Day",
|
||||
dataIndex: "num_requests_per_day",
|
||||
key: "num_requests_per_day",
|
||||
width: "15%",
|
||||
render: (_: number | undefined, record: ModelEntry) => (
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={record.num_requests_per_day}
|
||||
onChange={(value) => handleEntryChange(record.id, "num_requests_per_day", value ?? undefined)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
placeholder="-"
|
||||
formatter={(value) => value ? `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ""}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Requests/Month",
|
||||
dataIndex: "num_requests_per_month",
|
||||
key: "num_requests_per_month",
|
||||
width: "15%",
|
||||
render: (_: number | undefined, record: ModelEntry) => (
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={record.num_requests_per_month}
|
||||
onChange={(value) => handleEntryChange(record.id, "num_requests_per_month", value ?? undefined)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
placeholder="-"
|
||||
formatter={(value) => value ? `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ""}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
key: "actions",
|
||||
width: 50,
|
||||
render: (_: unknown, record: ModelEntry) => (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemoveEntry(record.id)}
|
||||
disabled={entries.length === 1}
|
||||
danger
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Add models to estimate costs. Each model can have its own token counts and request volumes.
|
||||
</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
icon={PlusOutlined}
|
||||
onClick={handleAddEntry}
|
||||
>
|
||||
Add Model
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{entries.map((entry) => (
|
||||
<ModelEntryRow
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
models={models}
|
||||
onChange={handleEntryChange}
|
||||
onRemove={handleRemoveEntry}
|
||||
canRemove={entries.length > 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={entries}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
footer={() => (
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={handleAddEntry}
|
||||
icon={<PlusOutlined />}
|
||||
className="w-full"
|
||||
>
|
||||
Add Another Model
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<MultiCostResults multiResult={multiModelResult} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
import React from "react";
|
||||
import { Select, InputNumber, Button, Tooltip } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { ModelEntry } from "./types";
|
||||
|
||||
interface ModelEntryRowProps {
|
||||
entry: ModelEntry;
|
||||
models: string[];
|
||||
onChange: (id: string, field: keyof ModelEntry, value: string | number | undefined) => void;
|
||||
onRemove: (id: string) => void;
|
||||
canRemove: boolean;
|
||||
}
|
||||
|
||||
const ModelEntryRow: React.FC<ModelEntryRowProps> = ({
|
||||
entry,
|
||||
models,
|
||||
onChange,
|
||||
onRemove,
|
||||
canRemove,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex-1 grid grid-cols-5 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">Model</label>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Select model"
|
||||
value={entry.model || undefined}
|
||||
onChange={(value) => onChange(entry.id, "model", value)}
|
||||
optionFilterProp="label"
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={models.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
}))}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">Input Tokens</label>
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={entry.input_tokens}
|
||||
onChange={(value) => onChange(entry.id, "input_tokens", value ?? 0)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">Output Tokens</label>
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={entry.output_tokens}
|
||||
onChange={(value) => onChange(entry.id, "output_tokens", value ?? 0)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">Requests/Day</label>
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={entry.num_requests_per_day}
|
||||
onChange={(value) => onChange(entry.id, "num_requests_per_day", value ?? undefined)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
placeholder="Optional"
|
||||
formatter={(value) => value ? `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ""}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">Requests/Month</label>
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={entry.num_requests_per_month}
|
||||
onChange={(value) => onChange(entry.id, "num_requests_per_month", value ?? undefined)}
|
||||
style={{ width: "100%" }}
|
||||
size="small"
|
||||
placeholder="Optional"
|
||||
formatter={(value) => value ? `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-5">
|
||||
<Tooltip title={canRemove ? "Remove model" : "At least one model required"}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => onRemove(entry.id)}
|
||||
disabled={!canRemove}
|
||||
danger
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelEntryRow;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { Text, Button } from "@tremor/react";
|
||||
import { Card, Statistic, Row, Col, Divider, Spin, Table, Tag } from "antd";
|
||||
import { DollarOutlined, LoadingOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import { LoadingOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import { CostEstimateResponse } from "../types";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { MultiModelResult } from "./types";
|
||||
|
|
@ -29,7 +29,7 @@ const SingleModelBreakdown: React.FC<{
|
|||
loading: boolean;
|
||||
}> = ({ result, loading }) => {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 bg-gray-50 p-4 rounded-lg">
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm">
|
||||
<Spin indicator={<LoadingOutlined spin />} size="small" />
|
||||
|
|
@ -37,136 +37,82 @@ const SingleModelBreakdown: React.FC<{
|
|||
</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: "16px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Input Cost"
|
||||
value={formatCost(result.input_cost_per_request)}
|
||||
valueStyle={{ fontSize: "14px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Output Cost"
|
||||
value={formatCost(result.output_cost_per_request)}
|
||||
valueStyle={{ fontSize: "14px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Margin/Fee"
|
||||
value={formatCost(result.margin_cost_per_request)}
|
||||
valueStyle={{
|
||||
fontSize: "14px",
|
||||
color: result.margin_cost_per_request > 0 ? "#faad14" : undefined,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Total/Request</Text>
|
||||
<Text className="text-base font-semibold text-blue-600">{formatCost(result.cost_per_request)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Input Cost</Text>
|
||||
<Text className="text-sm">{formatCost(result.input_cost_per_request)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Output Cost</Text>
|
||||
<Text className="text-sm">{formatCost(result.output_cost_per_request)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Margin/Fee</Text>
|
||||
<Text className={`text-sm ${result.margin_cost_per_request > 0 ? "text-amber-600" : ""}`}>
|
||||
{formatCost(result.margin_cost_per_request)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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: "16px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Input Cost"
|
||||
value={formatCost(result.daily_input_cost)}
|
||||
valueStyle={{ fontSize: "14px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Output Cost"
|
||||
value={formatCost(result.daily_output_cost)}
|
||||
valueStyle={{ fontSize: "14px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Margin/Fee"
|
||||
value={formatCost(result.daily_margin_cost)}
|
||||
valueStyle={{
|
||||
fontSize: "14px",
|
||||
color: (result.daily_margin_cost ?? 0) > 0 ? "#faad14" : undefined,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<div className="grid grid-cols-4 gap-4 pt-2 border-t border-gray-200">
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Daily Total ({formatRequests(result.num_requests_per_day)} req)</Text>
|
||||
<Text className="text-base font-semibold text-green-600">{formatCost(result.daily_cost)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Daily Input</Text>
|
||||
<Text className="text-sm">{formatCost(result.daily_input_cost)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Daily Output</Text>
|
||||
<Text className="text-sm">{formatCost(result.daily_output_cost)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Daily Margin</Text>
|
||||
<Text className={`text-sm ${(result.daily_margin_cost ?? 0) > 0 ? "text-amber-600" : ""}`}>
|
||||
{formatCost(result.daily_margin_cost)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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: "16px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Input Cost"
|
||||
value={formatCost(result.monthly_input_cost)}
|
||||
valueStyle={{ fontSize: "14px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Output Cost"
|
||||
value={formatCost(result.monthly_output_cost)}
|
||||
valueStyle={{ fontSize: "14px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Margin/Fee"
|
||||
value={formatCost(result.monthly_margin_cost)}
|
||||
valueStyle={{
|
||||
fontSize: "14px",
|
||||
color: (result.monthly_margin_cost ?? 0) > 0 ? "#faad14" : undefined,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<div className="grid grid-cols-4 gap-4 pt-2 border-t border-gray-200">
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Monthly Total ({formatRequests(result.num_requests_per_month)} req)</Text>
|
||||
<Text className="text-base font-semibold text-purple-600">{formatCost(result.monthly_cost)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Monthly Input</Text>
|
||||
<Text className="text-sm">{formatCost(result.monthly_input_cost)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Monthly Output</Text>
|
||||
<Text className="text-sm">{formatCost(result.monthly_output_cost)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-xs text-gray-500 block">Monthly Margin</Text>
|
||||
<Text className={`text-sm ${(result.monthly_margin_cost ?? 0) > 0 ? "text-amber-600" : ""}`}>
|
||||
{formatCost(result.monthly_margin_cost)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(result.input_cost_per_token || result.output_cost_per_token) && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Token Pricing: </span>
|
||||
<div className="text-xs text-gray-400 pt-2 border-t border-gray-200">
|
||||
Token Pricing: {" "}
|
||||
{result.input_cost_per_token && (
|
||||
<span>Input: ${formatNumberWithCommas(result.input_cost_per_token * 1_000_000, 2)}/1M</span>
|
||||
<span>Input ${formatNumberWithCommas(result.input_cost_per_token * 1_000_000, 2)}/1M</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</span>
|
||||
<span>Output ${formatNumberWithCommas(result.output_cost_per_token * 1_000_000, 2)}/1M</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -184,7 +130,7 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult }) => {
|
|||
|
||||
if (!hasAnyResult && !isAnyLoading) {
|
||||
return (
|
||||
<div className="py-8 text-center border border-dashed border-gray-300 rounded-lg">
|
||||
<div className="py-6 text-center border border-dashed border-gray-300 rounded-lg bg-gray-50">
|
||||
<Text className="text-gray-500">
|
||||
Select models above to see cost estimates
|
||||
</Text>
|
||||
|
|
@ -194,7 +140,7 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult }) => {
|
|||
|
||||
if (!hasAnyResult && isAnyLoading) {
|
||||
return (
|
||||
<div className="py-8 text-center">
|
||||
<div className="py-6 text-center">
|
||||
<Spin indicator={<LoadingOutlined spin />} />
|
||||
<Text className="text-gray-500 block mt-2">Calculating costs...</Text>
|
||||
</div>
|
||||
|
|
@ -213,16 +159,18 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const hasMargin = multiResult.totals.margin_per_request > 0;
|
||||
|
||||
const summaryColumns = [
|
||||
{
|
||||
title: "Model",
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
render: (text: string, record: { id: string; provider?: string | null }) => (
|
||||
<div>
|
||||
<span className="font-medium">{text}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{text}</span>
|
||||
{record.provider && (
|
||||
<Tag color="blue" className="ml-2 text-xs">
|
||||
<Tag color="blue" className="text-xs">
|
||||
{record.provider}
|
||||
</Tag>
|
||||
)}
|
||||
|
|
@ -233,29 +181,44 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult }) => {
|
|||
title: "Per Request",
|
||||
dataIndex: "cost_per_request",
|
||||
key: "cost_per_request",
|
||||
render: (value: number) => formatCost(value),
|
||||
align: "right" as const,
|
||||
render: (value: number) => <span className="font-mono text-sm">{formatCost(value)}</span>,
|
||||
},
|
||||
{
|
||||
title: "Margin",
|
||||
dataIndex: "margin_cost_per_request",
|
||||
key: "margin_cost_per_request",
|
||||
align: "right" as const,
|
||||
render: (value: number) => (
|
||||
<span className={`font-mono text-sm ${value > 0 ? "text-amber-600" : "text-gray-400"}`}>
|
||||
{formatCost(value)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Daily",
|
||||
dataIndex: "daily_cost",
|
||||
key: "daily_cost",
|
||||
render: (value: number | null) => formatCost(value),
|
||||
align: "right" as const,
|
||||
render: (value: number | null) => <span className="font-mono text-sm">{formatCost(value)}</span>,
|
||||
},
|
||||
{
|
||||
title: "Monthly",
|
||||
dataIndex: "monthly_cost",
|
||||
key: "monthly_cost",
|
||||
render: (value: number | null) => formatCost(value),
|
||||
align: "right" as const,
|
||||
render: (value: number | null) => <span className="font-mono text-sm">{formatCost(value)}</span>,
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
key: "expand",
|
||||
width: 50,
|
||||
width: 40,
|
||||
render: (_: unknown, record: { id: string }) => (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
onClick={() => toggleExpanded(record.id)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{expandedModels.has(record.id) ? <DownOutlined /> : <RightOutlined />}
|
||||
</Button>
|
||||
|
|
@ -269,88 +232,95 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult }) => {
|
|||
model: e.result!.model,
|
||||
provider: e.result!.provider,
|
||||
cost_per_request: e.result!.cost_per_request,
|
||||
margin_cost_per_request: e.result!.margin_cost_per_request,
|
||||
daily_cost: e.result!.daily_cost,
|
||||
monthly_cost: e.result!.monthly_cost,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Divider />
|
||||
<Divider className="my-4" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Text className="text-lg font-semibold text-gray-900">Cost Estimates</Text>
|
||||
<Text className="text-sm text-gray-500 block mt-1">
|
||||
{validEntries.length} model{validEntries.length !== 1 ? "s" : ""} configured
|
||||
</Text>
|
||||
</div>
|
||||
<Text className="text-base font-semibold text-gray-900">Cost Estimates</Text>
|
||||
<div className="flex items-center gap-2">
|
||||
{isAnyLoading && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
<MultiExportDropdown multiResult={multiResult} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Totals Summary */}
|
||||
{validEntries.length > 1 && (
|
||||
<Card size="small" className="bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className="font-semibold text-gray-800">Combined Totals</Text>
|
||||
</div>
|
||||
<Row gutter={16} className="mt-3">
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="Total Per Request"
|
||||
value={formatCost(multiResult.totals.cost_per_request)}
|
||||
valueStyle={{ color: "#1890ff", fontSize: "20px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
{/* Combined Totals - Always show when there are results */}
|
||||
<Card size="small" className="bg-gradient-to-r from-slate-50 to-blue-50 border-slate-200">
|
||||
<Row gutter={[16, 8]}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Statistic
|
||||
title={<span className="text-xs">Total Per Request</span>}
|
||||
value={formatCost(multiResult.totals.cost_per_request)}
|
||||
valueStyle={{ color: "#1890ff", fontSize: "18px", fontFamily: "monospace" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Statistic
|
||||
title={<span className="text-xs">Total Daily</span>}
|
||||
value={formatCost(multiResult.totals.daily_cost)}
|
||||
valueStyle={{ color: "#52c41a", fontSize: "18px", fontFamily: "monospace" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Statistic
|
||||
title={<span className="text-xs">Total Monthly</span>}
|
||||
value={formatCost(multiResult.totals.monthly_cost)}
|
||||
valueStyle={{ color: "#722ed1", fontSize: "18px", fontFamily: "monospace" }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{hasMargin && (
|
||||
<Row gutter={[16, 8]} className="mt-3 pt-3 border-t border-slate-200">
|
||||
<Col xs={24} sm={8}>
|
||||
<div className="text-xs text-gray-500">Margin/Request</div>
|
||||
<div className="text-sm font-mono text-amber-600">{formatCost(multiResult.totals.margin_per_request)}</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="Total Daily"
|
||||
value={formatCost(multiResult.totals.daily_cost)}
|
||||
valueStyle={{ color: "#52c41a", fontSize: "20px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
<Col xs={24} sm={8}>
|
||||
<div className="text-xs text-gray-500">Daily Margin</div>
|
||||
<div className="text-sm font-mono text-amber-600">{formatCost(multiResult.totals.daily_margin)}</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="Total Monthly"
|
||||
value={formatCost(multiResult.totals.monthly_cost)}
|
||||
valueStyle={{ color: "#722ed1", fontSize: "20px" }}
|
||||
prefix={<DollarOutlined />}
|
||||
/>
|
||||
<Col xs={24} sm={8}>
|
||||
<div className="text-xs text-gray-500">Monthly Margin</div>
|
||||
<div className="text-sm font-mono text-amber-600">{formatCost(multiResult.totals.monthly_margin)}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Per-Model Summary Table */}
|
||||
<Table
|
||||
columns={summaryColumns}
|
||||
dataSource={summaryData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
expandable={{
|
||||
expandedRowKeys: Array.from(expandedModels),
|
||||
expandedRowRender: (record) => {
|
||||
const entry = validEntries.find((e) => e.entry.id === record.id);
|
||||
if (!entry?.result) return null;
|
||||
return (
|
||||
<div className="py-4 px-2">
|
||||
<SingleModelBreakdown result={entry.result} loading={entry.loading} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
showExpandColumn: false,
|
||||
}}
|
||||
/>
|
||||
{/* Per-Model Table */}
|
||||
{validEntries.length > 0 && (
|
||||
<Table
|
||||
columns={summaryColumns}
|
||||
dataSource={summaryData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
className="border border-gray-200 rounded-lg"
|
||||
expandable={{
|
||||
expandedRowKeys: Array.from(expandedModels),
|
||||
expandedRowRender: (record) => {
|
||||
const entry = validEntries.find((e) => e.entry.id === record.id);
|
||||
if (!entry?.result) return null;
|
||||
return (
|
||||
<div className="py-2">
|
||||
<SingleModelBreakdown result={entry.result} loading={entry.loading} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
showExpandColumn: false,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Error Messages */}
|
||||
{multiResult.entries
|
||||
.filter((e) => e.error)
|
||||
.map((e) => (
|
||||
<div key={e.entry.id} className="text-sm text-red-500 bg-red-50 p-2 rounded">
|
||||
<div key={e.entry.id} className="text-sm text-red-600 bg-red-50 p-3 rounded-lg border border-red-200">
|
||||
<span className="font-medium">{e.entry.model || "Unknown model"}: </span>
|
||||
{e.error}
|
||||
</div>
|
||||
|
|
@ -360,4 +330,3 @@ const MultiCostResults: React.FC<MultiCostResultsProps> = ({ multiResult }) => {
|
|||
};
|
||||
|
||||
export default MultiCostResults;
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,6 @@ export const exportMultiToPDF = (multiResult: MultiModelResult): void => {
|
|||
<h1>LLM Cost Estimate Report</h1>
|
||||
<p style="color: #666; margin-top: -20px; margin-bottom: 30px;">${modelCount} model${modelCount !== 1 ? "s" : ""} configured</p>
|
||||
|
||||
${modelCount > 1 ? `
|
||||
<div class="summary-box">
|
||||
<h2>Combined Totals</h2>
|
||||
<div class="summary-grid">
|
||||
|
|
@ -213,8 +212,23 @@ export const exportMultiToPDF = (multiResult: MultiModelResult): void => {
|
|||
<div class="value purple">${formatCostForExport(multiResult.totals.monthly_cost)}</div>
|
||||
</div>
|
||||
</div>
|
||||
${multiResult.totals.margin_per_request > 0 ? `
|
||||
<div class="summary-grid" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<div class="summary-item">
|
||||
<div class="label">Margin/Request</div>
|
||||
<div class="value" style="color: #faad14;">${formatCostForExport(multiResult.totals.margin_per_request)}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="label">Daily Margin</div>
|
||||
<div class="value" style="color: #faad14;">${formatCostForExport(multiResult.totals.daily_margin)}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="label">Monthly Margin</div>
|
||||
<div class="value" style="color: #faad14;">${formatCostForExport(multiResult.totals.monthly_margin)}</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<h2>Model Breakdown</h2>
|
||||
${validEntries.map((e) => generateModelSection(e.result!)).join("")}
|
||||
|
|
@ -243,15 +257,16 @@ export const exportMultiToCSV = (multiResult: MultiModelResult): void => {
|
|||
];
|
||||
|
||||
// Summary section
|
||||
if (validEntries.length > 1) {
|
||||
rows.push(
|
||||
["COMBINED TOTALS"],
|
||||
["Total Per Request", multiResult.totals.cost_per_request.toString()],
|
||||
["Total Daily", multiResult.totals.daily_cost?.toString() || "-"],
|
||||
["Total Monthly", multiResult.totals.monthly_cost?.toString() || "-"],
|
||||
[""]
|
||||
);
|
||||
}
|
||||
rows.push(
|
||||
["COMBINED TOTALS"],
|
||||
["Total Per Request", multiResult.totals.cost_per_request.toString()],
|
||||
["Total Daily", multiResult.totals.daily_cost?.toString() || "-"],
|
||||
["Total Monthly", multiResult.totals.monthly_cost?.toString() || "-"],
|
||||
["Margin Per Request", multiResult.totals.margin_per_request.toString()],
|
||||
["Daily Margin", multiResult.totals.daily_margin?.toString() || "-"],
|
||||
["Monthly Margin", multiResult.totals.monthly_margin?.toString() || "-"],
|
||||
[""]
|
||||
);
|
||||
|
||||
// Summary table header
|
||||
rows.push([
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ export interface MultiModelResult {
|
|||
cost_per_request: number;
|
||||
daily_cost: number | null;
|
||||
monthly_cost: number | null;
|
||||
margin_per_request: number;
|
||||
daily_margin: number | null;
|
||||
monthly_margin: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,16 +159,26 @@ export function useMultiCostEstimate(accessToken: string | null) {
|
|||
let totalCostPerRequest = 0;
|
||||
let totalDailyCost: number | null = null;
|
||||
let totalMonthlyCost: number | null = null;
|
||||
let totalMarginPerRequest = 0;
|
||||
let totalDailyMargin: number | null = null;
|
||||
let totalMonthlyMargin: number | null = null;
|
||||
|
||||
for (const r of results) {
|
||||
if (r.result) {
|
||||
totalCostPerRequest += r.result.cost_per_request;
|
||||
totalMarginPerRequest += r.result.margin_cost_per_request;
|
||||
if (r.result.daily_cost !== null) {
|
||||
totalDailyCost = (totalDailyCost ?? 0) + r.result.daily_cost;
|
||||
}
|
||||
if (r.result.daily_margin_cost !== null) {
|
||||
totalDailyMargin = (totalDailyMargin ?? 0) + r.result.daily_margin_cost;
|
||||
}
|
||||
if (r.result.monthly_cost !== null) {
|
||||
totalMonthlyCost = (totalMonthlyCost ?? 0) + r.result.monthly_cost;
|
||||
}
|
||||
if (r.result.monthly_margin_cost !== null) {
|
||||
totalMonthlyMargin = (totalMonthlyMargin ?? 0) + r.result.monthly_margin_cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +188,9 @@ export function useMultiCostEstimate(accessToken: string | null) {
|
|||
cost_per_request: totalCostPerRequest,
|
||||
daily_cost: totalDailyCost,
|
||||
monthly_cost: totalMonthlyCost,
|
||||
margin_per_request: totalMarginPerRequest,
|
||||
daily_margin: totalDailyMargin,
|
||||
monthly_margin: totalMonthlyMargin,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue