v0 cost estimator

This commit is contained in:
Ishaan Jaffer 2026-01-05 17:45:29 +05:30
parent 59ec4b53b2
commit 5123ec0815
6 changed files with 463 additions and 0 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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 };
}

View file

@ -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;
}