diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.test.tsx b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.test.tsx
index f31e1839739..4ba6d8d50b4 100644
--- a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.test.tsx
+++ b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.test.tsx
@@ -51,4 +51,23 @@ describe("ResponseMetrics prompt cache chips", () => {
expect(screen.queryByText(/Response Cache/)).not.toBeInTheDocument();
});
+
+ it("does not render the Cost chip when a persisted cost is null", () => {
+ render();
+
+ expect(screen.queryByText(/Cost:/)).not.toBeInTheDocument();
+ expect(screen.getByText("In: 1")).toBeInTheDocument();
+ });
+
+ it("does not render the Cost chip for NaN", () => {
+ render();
+
+ expect(screen.queryByText(/Cost:/)).not.toBeInTheDocument();
+ });
+
+ it("renders the Cost chip for a finite cost", () => {
+ render();
+
+ expect(screen.getByText("Cost: $0.000063")).toBeInTheDocument();
+ });
});
diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx
index ec62d0618d7..bb7debc7aee 100644
--- a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx
+++ b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx
@@ -159,7 +159,7 @@ const ResponseMetrics: React.FC = ({ timeToFirstToken, tot
/>
)}
- {usage?.cost !== undefined && (
+ {typeof usage?.cost === "number" && Number.isFinite(usage.cost) && (
{
expect(usageData).not.toHaveProperty("cacheReadTokens");
expect(usageData).not.toHaveProperty("cacheCreationTokens");
});
+
+ it("omits cost when the provider reports a non-numeric value", async () => {
+ const usageData = await captureUsage({ cost: "not-a-number" });
+
+ expect(usageData).toEqual(expect.not.objectContaining({ cost: expect.anything() }));
+ });
+
+ it("omits cost when the provider reports a blank value", async () => {
+ const usageData = await captureUsage({ cost: " " });
+
+ expect(usageData).toEqual(expect.not.objectContaining({ cost: expect.anything() }));
+ });
});
describe("chat_completion response cache", () => {
diff --git a/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx
index cd0852bc06e..ffa2877fbd9 100644
--- a/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx
+++ b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx
@@ -5,6 +5,7 @@ import { VectorStoreSearchResponse } from "../chat_ui/types";
import { getProxyBaseUrl } from "@/components/networking";
import { MCPServer, MCPToolset, type MCPEvent } from "@/components/mcp_tools/types";
import { extractPromptCacheTokens } from "@/utils/promptCacheUsage";
+import { parseUsageCost } from "./usage_cost";
const completionAsSingleChunk = (completion: ChatCompletion): ChatCompletionChunk =>
({
@@ -243,9 +244,9 @@ export async function makeOpenAIChatCompletionRequest(
usageData.reasoningTokens = chunkWithUsage.usage.completion_tokens_details.reasoning_tokens;
}
- // Extract cost from usage object if available
- if (chunkWithUsage.usage.cost !== undefined && chunkWithUsage.usage.cost !== null) {
- usageData.cost = parseFloat(chunkWithUsage.usage.cost);
+ const parsedCost = parseUsageCost(chunkWithUsage.usage.cost);
+ if (parsedCost !== undefined) {
+ usageData.cost = parsedCost;
}
onUsageData(usageData);
diff --git a/ui/litellm-dashboard/src/components/llm_calls/responses_api.test.tsx b/ui/litellm-dashboard/src/components/llm_calls/responses_api.test.tsx
index ae5e224cfbf..290c8b0b619 100644
--- a/ui/litellm-dashboard/src/components/llm_calls/responses_api.test.tsx
+++ b/ui/litellm-dashboard/src/components/llm_calls/responses_api.test.tsx
@@ -233,6 +233,64 @@ describe("responses_api", () => {
expect(onUsageData).toHaveBeenCalledWith(expect.not.objectContaining({ cost: expect.anything() }), "");
});
+ it("should omit cost when the proxy reports a non-numeric cost", async () => {
+ async function* streamWithNonNumericCost() {
+ yield {
+ type: "response.completed",
+ response: {
+ id: "resp_non_numeric_cost",
+ usage: { output_tokens: 12, input_tokens: 12, total_tokens: 24, cost: "not-a-number" },
+ },
+ };
+ }
+ mockResponsesCreate.mockResolvedValueOnce(streamWithNonNumericCost());
+
+ const onUsageData = vi.fn();
+
+ await makeOpenAIResponsesRequest(
+ messages,
+ mockUpdateTextUI,
+ "gpt-4",
+ "test-token",
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ onUsageData,
+ );
+
+ expect(onUsageData).toHaveBeenCalledWith(expect.not.objectContaining({ cost: expect.anything() }), "");
+ });
+
+ it("should omit cost when the proxy reports a blank cost", async () => {
+ async function* streamWithBlankCost() {
+ yield {
+ type: "response.completed",
+ response: {
+ id: "resp_blank_cost",
+ usage: { output_tokens: 12, input_tokens: 12, total_tokens: 24, cost: " " },
+ },
+ };
+ }
+ mockResponsesCreate.mockResolvedValueOnce(streamWithBlankCost());
+
+ const onUsageData = vi.fn();
+
+ await makeOpenAIResponsesRequest(
+ messages,
+ mockUpdateTextUI,
+ "gpt-4",
+ "test-token",
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ onUsageData,
+ );
+
+ expect(onUsageData).toHaveBeenCalledWith(expect.not.objectContaining({ cost: expect.anything() }), "");
+ });
+
it("should replay MCP output items as events for a non-streaming response", async () => {
mockResponsesCreate.mockReturnValueOnce(
nonStreamingResponse({
diff --git a/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx b/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx
index 7ab76488504..ce54c7c6b40 100644
--- a/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx
+++ b/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx
@@ -4,6 +4,7 @@ import { TokenUsage } from "../chat_ui/ResponseMetrics";
import { getProxyBaseUrl } from "@/components/networking";
import { toast } from "@/lib/toast";
import { extractPromptCacheTokens } from "@/utils/promptCacheUsage";
+import { parseUsageCost } from "./usage_cost";
import type { MCPEvent } from "@/components/mcp_tools/types";
import { MCPServer, MCPToolset } from "@/components/mcp_tools/types";
import {
@@ -311,8 +312,9 @@ export async function makeOpenAIResponsesRequest(
usageData.reasoningTokens = reasoningTokens;
}
- if (usage.cost !== undefined && usage.cost !== null) {
- usageData.cost = Number(usage.cost);
+ const parsedCost = parseUsageCost(usage.cost);
+ if (parsedCost !== undefined) {
+ usageData.cost = parsedCost;
}
onUsageData(usageData, mcpToolUsed);
diff --git a/ui/litellm-dashboard/src/components/llm_calls/usage_cost.test.ts b/ui/litellm-dashboard/src/components/llm_calls/usage_cost.test.ts
new file mode 100644
index 00000000000..ee1021c0629
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/llm_calls/usage_cost.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import { parseUsageCost } from "./usage_cost";
+
+describe("parseUsageCost", () => {
+ it("keeps finite numbers, including zero", () => {
+ expect(parseUsageCost(0)).toBe(0);
+ expect(parseUsageCost(0.000063)).toBe(0.000063);
+ });
+
+ it("keeps numeric strings", () => {
+ expect(parseUsageCost("0.00019")).toBe(0.00019);
+ expect(parseUsageCost(" 0.00019 ")).toBe(0.00019);
+ });
+
+ it("drops blank strings instead of fabricating a zero cost", () => {
+ expect(parseUsageCost("")).toBeUndefined();
+ expect(parseUsageCost(" ")).toBeUndefined();
+ expect(parseUsageCost("\t\n")).toBeUndefined();
+ });
+
+ it("drops strings with a numeric prefix instead of truncating them", () => {
+ expect(parseUsageCost("1oops")).toBeUndefined();
+ expect(parseUsageCost("0.5 USD")).toBeUndefined();
+ });
+
+ it("drops non-finite numbers", () => {
+ expect(parseUsageCost(Number.NaN)).toBeUndefined();
+ expect(parseUsageCost(Number.POSITIVE_INFINITY)).toBeUndefined();
+ });
+
+ it("drops values that are not numbers or strings", () => {
+ expect(parseUsageCost(null)).toBeUndefined();
+ expect(parseUsageCost(undefined)).toBeUndefined();
+ expect(parseUsageCost(true)).toBeUndefined();
+ expect(parseUsageCost([])).toBeUndefined();
+ expect(parseUsageCost(["0.5"])).toBeUndefined();
+ expect(parseUsageCost({ total_cost: 0.5 })).toBeUndefined();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/llm_calls/usage_cost.ts b/ui/litellm-dashboard/src/components/llm_calls/usage_cost.ts
new file mode 100644
index 00000000000..79f56dcb2df
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/llm_calls/usage_cost.ts
@@ -0,0 +1,23 @@
+/**
+ * Providers and upstream gateways report `usage.cost` unvalidated: it arrives as a number, a numeric
+ * string, an empty string, or something non-numeric. A cost that does not resolve to a finite number
+ * must be dropped rather than coerced, because `NaN` survives `JSON.stringify` as `null` and crashes
+ * the metrics row on the next load.
+ */
+export function parseUsageCost(rawCost: unknown): number | undefined {
+ if (typeof rawCost === "number") {
+ return Number.isFinite(rawCost) ? rawCost : undefined;
+ }
+
+ if (typeof rawCost !== "string") {
+ return undefined;
+ }
+
+ const trimmed = rawCost.trim();
+ if (trimmed === "") {
+ return undefined;
+ }
+
+ const parsed = Number(trimmed);
+ return Number.isFinite(parsed) ? parsed : undefined;
+}