Merge pull request #21152 from BerriAI/litellm_opus46_cost_cal

[Fix] UI - Spend Logs: Cost Calculation
This commit is contained in:
yuneng-jiang
2026-02-20 15:19:49 -08:00
committed by GitHub
4 changed files with 451 additions and 36 deletions
@@ -19,6 +19,9 @@ export interface CostBreakdown {
interface CostBreakdownViewerProps {
costBreakdown: CostBreakdown | null | undefined;
totalSpend: number;
promptTokens?: number;
completionTokens?: number;
cacheHit?: string;
}
const formatCost = (cost: number | undefined): string => {
@@ -34,31 +37,45 @@ const formatPercent = (percent: number | undefined): string => {
export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
costBreakdown,
totalSpend,
promptTokens,
completionTokens,
cacheHit,
}) => {
if (!costBreakdown) {
return null;
}
const isCached = cacheHit?.toLowerCase() === "true";
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
const hasDiscount =
(costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0) ||
(costBreakdown.discount_amount !== undefined && costBreakdown.discount_amount !== 0);
const hasMargin =
(costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0) ||
(costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0) ||
(costBreakdown.margin_total_amount !== undefined && costBreakdown.margin_total_amount !== 0);
// Don't show if there's no meaningful breakdown data
const hasCostBreakdown = costBreakdown?.input_cost !== undefined || costBreakdown?.output_cost !== undefined;
const hasMeaningfulData =
costBreakdown.input_cost !== undefined ||
costBreakdown.output_cost !== undefined ||
hasDiscount ||
hasMargin;
hasCostBreakdown ||
hasTokenCounts ||
(costBreakdown &&
((costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0) ||
(costBreakdown.discount_amount !== undefined && costBreakdown.discount_amount !== 0) ||
(costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0) ||
(costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0) ||
(costBreakdown.margin_total_amount !== undefined && costBreakdown.margin_total_amount !== 0)));
if (!hasMeaningfulData) {
return null;
}
const hasDiscount =
costBreakdown &&
((costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0) ||
(costBreakdown.discount_amount !== undefined && costBreakdown.discount_amount !== 0));
const hasMargin =
costBreakdown &&
((costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0) ||
(costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0) ||
(costBreakdown.margin_total_amount !== undefined && costBreakdown.margin_total_amount !== 0));
// When cached, show $0 (authoritative total) instead of pre-cache costs from cost_breakdown
const inputCost = isCached ? 0 : costBreakdown?.input_cost;
const outputCost = isCached ? 0 : costBreakdown?.output_cost;
const originalCost = isCached ? 0 : costBreakdown?.original_cost;
const totalCost = isCached ? 0 : (costBreakdown?.total_cost ?? totalSpend);
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Collapse
@@ -71,7 +88,10 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
<div className="flex items-center space-x-2 mr-4">
<span className="text-sm text-gray-500">Total:</span>
<span className="text-sm font-semibold text-gray-900">{formatCost(totalSpend)}</span>
<span className="text-sm font-semibold text-gray-900">
{formatCost(totalSpend)}
{isCached && " (Cached)"}
</span>
</div>
</div>
),
@@ -81,20 +101,34 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
<div className="space-y-2 max-w-2xl">
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.input_cost)}</span>
<span className="text-gray-900">
{formatCost(inputCost)}
{promptTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({promptTokens.toLocaleString()} prompt tokens)
</span>
)}
</span>
</div>
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.output_cost)}</span>
<span className="text-gray-900">
{formatCost(outputCost)}
{completionTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens)
</span>
)}
</span>
</div>
{costBreakdown.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
</div>
)}
{/* Additional Costs (free-form) */}
{costBreakdown.additional_costs && Object.keys(costBreakdown.additional_costs).length > 0 && (
{costBreakdown?.additional_costs && Object.keys(costBreakdown.additional_costs).length > 0 && (
<>
{Object.entries(costBreakdown.additional_costs).map(([key, value]) => (
<div key={key} className="flex text-sm">
@@ -106,13 +140,15 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
)}
</div>
{/* Subtotal / Original Cost */}
<div className="pt-2 border-t border-gray-100 max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.original_cost)}</span>
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
{!isCached && (
<div className="pt-2 border-t border-gray-100 max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
<span className="text-gray-900">{formatCost(originalCost)}</span>
</div>
</div>
</div>
)}
{/* Step 2: Adjustments (Discount & Margin) */}
{(hasDiscount || hasMargin) && (
@@ -160,7 +196,8 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
<div className="flex items-center">
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
<span className="text-sm font-bold text-gray-900">
{formatCost(costBreakdown.total_cost ?? totalSpend)}
{formatCost(totalCost)}
{isCached && " (Cached)"}
</span>
</div>
</div>
@@ -0,0 +1,346 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { LogDetailContent } from "./LogDetailContent";
import type { LogEntry } from "../columns";
vi.mock("../GuardrailViewer/GuardrailViewer", () => ({
default: ({ data }: { data: unknown }) => <div data-testid="guardrail-viewer">{JSON.stringify(data)}</div>,
}));
const createLogEntry = (overrides: Partial<LogEntry> = {}): LogEntry =>
({
request_id: "chatcmpl-test-id",
api_key: "api-key",
team_id: "team-id",
model: "gpt-4",
model_id: "gpt-4",
call_type: "chat",
spend: 0,
total_tokens: 10,
prompt_tokens: 5,
completion_tokens: 5,
startTime: "2025-11-14T00:00:00Z",
endTime: "2025-11-14T00:00:01Z",
cache_hit: "miss",
duration: 1,
messages: [{ role: "user", content: "hello" }],
response: { choices: [{ message: { content: "hi" } }] },
metadata: { status: "success" },
request_tags: {},
custom_llm_provider: "openai",
api_base: "https://api.example.com",
...overrides,
}) as LogEntry;
describe("LogDetailContent", () => {
it("should render the component successfully", () => {
render(<LogDetailContent logEntry={createLogEntry()} />);
expect(screen.getByText("Request Details")).toBeInTheDocument();
});
it("should display Request Details with model, provider, and call type", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
model: "gpt-4o",
custom_llm_provider: "anthropic",
call_type: "completion",
})}
/>,
);
expect(screen.getByText("gpt-4o")).toBeInTheDocument();
expect(screen.getByText("anthropic")).toBeInTheDocument();
expect(screen.getByText("completion")).toBeInTheDocument();
});
it("should display error alert when request has failed", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
metadata: {
status: "failure",
error_information: {
error_code: "rate_limit",
error_message: "Too many requests",
error_class: "RateLimitError",
},
},
})}
/>,
);
expect(screen.getByText("Request Failed")).toBeInTheDocument();
expect(screen.getByText("rate_limit")).toBeInTheDocument();
expect(screen.getByText("Too many requests")).toBeInTheDocument();
});
it("should display tags section when request_tags has entries", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
request_tags: { env: "prod", version: "1.0" },
})}
/>,
);
expect(screen.getByText("Tags")).toBeInTheDocument();
expect(screen.getByText("env: prod")).toBeInTheDocument();
expect(screen.getByText("version: 1.0")).toBeInTheDocument();
});
it("should not display tags section when request_tags is empty", () => {
render(<LogDetailContent logEntry={createLogEntry({ request_tags: {} })} />);
expect(screen.queryByText("Tags")).not.toBeInTheDocument();
});
it("should display Metrics section with tokens and cost", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
spend: 0.002,
})}
/>,
);
expect(screen.getByText("Metrics")).toBeInTheDocument();
expect(screen.getAllByText("$0.00200000").length).toBeGreaterThanOrEqual(1);
});
it("should display ConfigInfoMessage when no messages, response, or error and not loading", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
messages: [],
response: {},
metadata: {},
})}
/>,
);
expect(screen.getByText("Request/Response Data Not Available")).toBeInTheDocument();
});
it("should not display ConfigInfoMessage when isLoadingDetails is true even without data", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
messages: [],
response: {},
metadata: {},
})}
isLoadingDetails={true}
/>,
);
expect(screen.queryByText("Request/Response Data Not Available")).not.toBeInTheDocument();
});
it("should call onOpenSettings when user clicks open settings in ConfigInfoMessage", async () => {
const onOpenSettings = vi.fn();
const user = userEvent.setup();
render(
<LogDetailContent
logEntry={createLogEntry({
messages: [],
response: {},
metadata: {},
})}
onOpenSettings={onOpenSettings}
/>,
);
const settingsButton = screen.getByRole("button", { name: /open the settings/i });
await user.click(settingsButton);
expect(onOpenSettings).toHaveBeenCalledTimes(1);
});
it("should display loading state when isLoadingDetails is true", () => {
render(
<LogDetailContent
logEntry={createLogEntry()}
isLoadingDetails={true}
/>,
);
expect(screen.getByText("Loading request & response data...")).toBeInTheDocument();
});
it("should display Request & Response section with Pretty and JSON view modes", () => {
render(<LogDetailContent logEntry={createLogEntry()} />);
expect(screen.getByText("Request & Response")).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Pretty" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "JSON" })).toBeInTheDocument();
});
it("should display Request and Response tabs when JSON view is selected", async () => {
const user = userEvent.setup();
render(<LogDetailContent logEntry={createLogEntry()} />);
await user.click(screen.getByText("JSON"));
expect(screen.getByRole("tab", { name: "Request" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Response" })).toBeInTheDocument();
});
it("should display response not available message when no response and Response tab is selected", async () => {
const user = userEvent.setup();
render(
<LogDetailContent
logEntry={createLogEntry({
response: {},
metadata: { status: "success" },
})}
/>,
);
await user.click(screen.getByText("JSON"));
await user.click(screen.getByRole("tab", { name: "Response" }));
expect(screen.getByText("Response data not available")).toBeInTheDocument();
});
it("should display Metadata section when metadata has keys", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
metadata: { status: "success", custom_key: "value" },
})}
/>,
);
expect(screen.getByText("Metadata")).toBeInTheDocument();
});
it("should display IP address when requester_ip_address is present", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
requester_ip_address: "192.168.1.1",
})}
/>,
);
expect(screen.getByText("192.168.1.1")).toBeInTheDocument();
});
it("should display guardrail label when guardrail data exists", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
metadata: {
status: "success",
guardrail_information: {
guardrail_name: "PII Filter",
masked_entity_count: { PERSON: 2 },
},
},
})}
/>,
);
expect(screen.getByText("PII Filter")).toBeInTheDocument();
expect(screen.getByText("2 masked")).toBeInTheDocument();
});
it("should display cache hit information when cache_hit is true", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
cache_hit: "true",
metadata: {
status: "success",
additional_usage_values: {
cache_read_input_tokens: 100,
cache_creation_input_tokens: 0,
},
},
})}
/>,
);
expect(screen.getByText("Cache Hit")).toBeInTheDocument();
expect(screen.getByText("true")).toBeInTheDocument();
expect(screen.getByText("Cache Read Tokens")).toBeInTheDocument();
expect(screen.getByText("100")).toBeInTheDocument();
});
it("should display LiteLLM Overhead when litellm_overhead_time_ms is in metadata", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
metadata: {
status: "success",
litellm_overhead_time_ms: 42.5,
},
})}
/>,
);
expect(screen.getByText("LiteLLM Overhead")).toBeInTheDocument();
expect(screen.getByText("42.50 ms")).toBeInTheDocument();
});
it("should display start and end time in ISO format", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
startTime: "2025-11-14T12:00:00.000Z",
endTime: "2025-11-14T12:00:01.500Z",
})}
/>,
);
expect(screen.getByText("Start Time")).toBeInTheDocument();
expect(screen.getByText("End Time")).toBeInTheDocument();
const dateElements = screen.getAllByText((content) => content.includes("2025-11-14"));
expect(dateElements.length).toBeGreaterThanOrEqual(2);
});
it("should display Vector Store Requests when vector store data exists", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
metadata: {
status: "success",
vector_store_request_metadata: [
{
query: "test query",
vector_store_id: "vs-123",
custom_llm_provider: "openai",
start_time: 1700000000,
end_time: 1700000001,
vector_store_search_response: { data: [], search_query: "test" },
},
],
},
})}
/>,
);
expect(screen.getByText("Vector Store Requests")).toBeInTheDocument();
});
it("should display provider as dash when custom_llm_provider is absent", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
custom_llm_provider: undefined,
})}
/>,
);
const descriptions = screen.getByText("Provider").closest(".ant-descriptions-item");
expect(descriptions).toBeInTheDocument();
expect(screen.getByText("-")).toBeInTheDocument();
});
});
@@ -137,7 +137,13 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
<MetricsSection logEntry={logEntry} metadata={metadata} />
{/* Cost Breakdown */}
<CostBreakdownViewer costBreakdown={metadata?.cost_breakdown} totalSpend={logEntry.spend || 0} />
<CostBreakdownViewer
costBreakdown={metadata?.cost_breakdown}
totalSpend={logEntry.spend ?? 0}
promptTokens={logEntry.prompt_tokens}
completionTokens={logEntry.completion_tokens}
cacheHit={logEntry.cache_hit}
/>
{/* Tools */}
<ToolsSection log={logEntry} />
@@ -258,9 +264,17 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
(metadata?.additional_usage_values?.cache_read_input_tokens &&
metadata.additional_usage_values.cache_read_input_tokens > 0);
const cacheHitValue = String(logEntry.cache_hit ?? "None");
const cacheHitColor =
cacheHitValue.toLowerCase() === "true"
? "green"
: cacheHitValue.toLowerCase() === "false"
? "red"
: "default";
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card title="Metrics" size="small" bordered={false} style={{ marginBottom: 0 }}>
<Card title="Metrics" size="small" style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small">
<Descriptions.Item label="Tokens">
<TokenFlow
@@ -275,7 +289,7 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
{hasCacheActivity && (
<>
<Descriptions.Item label="Cache Hit">
<Tag color={logEntry.cache_hit ? "green" : "default"}>{logEntry.cache_hit || "None"}</Tag>
<Tag color={cacheHitColor}>{cacheHitValue}</Tag>
</Descriptions.Item>
{metadata?.additional_usage_values?.cache_read_input_tokens > 0 && (
<Descriptions.Item label="Cache Read Tokens">
@@ -339,12 +353,24 @@ function RequestResponseSection({
return JSON.stringify(data, null, 2);
};
const totalSpend = logEntry.spend || 0;
const totalSpend = logEntry.spend ?? 0;
const promptTokens = logEntry.prompt_tokens || 0;
const completionTokens = logEntry.completion_tokens || 0;
const totalTokens = promptTokens + completionTokens;
const inputCost = totalTokens > 0 ? (totalSpend * promptTokens) / totalTokens : 0;
const outputCost = totalTokens > 0 ? (totalSpend * completionTokens) / totalTokens : 0;
const costBreakdown = logEntry.metadata?.cost_breakdown;
const useCostBreakdown =
costBreakdown?.input_cost !== undefined &&
costBreakdown?.output_cost !== undefined;
const inputCost = useCostBreakdown
? (costBreakdown!.input_cost ?? 0)
: totalTokens > 0
? (totalSpend * promptTokens) / totalTokens
: 0;
const outputCost = useCostBreakdown
? (costBreakdown!.output_cost ?? 0)
: totalTokens > 0
? (totalSpend * completionTokens) / totalTokens
: 0;
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
@@ -975,7 +975,13 @@ export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onO
</div>
{/* Cost Breakdown - Show if cost breakdown data is available */}
<CostBreakdownViewer costBreakdown={row.original.metadata?.cost_breakdown} totalSpend={row.original.spend || 0} />
<CostBreakdownViewer
costBreakdown={row.original.metadata?.cost_breakdown}
totalSpend={row.original.spend ?? 0}
promptTokens={row.original.prompt_tokens}
completionTokens={row.original.completion_tokens}
cacheHit={row.original.cache_hit}
/>
{/* Configuration Info Message - Show when data is missing */}
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />