mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-08 22:25:25 +00:00
Merge pull request #21152 from BerriAI/litellm_opus46_cost_cal
[Fix] UI - Spend Logs: Cost Calculation
This commit is contained in:
@@ -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>
|
||||
|
||||
+346
@@ -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();
|
||||
});
|
||||
});
|
||||
+32
-6
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user