fix(ui): guard playground cost metric against null and NaN

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
jesus 2026-09-08 15:13:53 +00:00
parent 1af7a403c6
commit 06497ab42a
6 changed files with 63 additions and 3 deletions

View file

@ -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(<ResponseMetrics usage={{ promptTokens: 1, cost: null as unknown as number }} />);
expect(screen.queryByText(/Cost:/)).not.toBeInTheDocument();
expect(screen.getByText("In: 1")).toBeInTheDocument();
});
it("does not render the Cost chip for NaN", () => {
render(<ResponseMetrics usage={{ ...baseUsage, cost: Number.NaN }} />);
expect(screen.queryByText(/Cost:/)).not.toBeInTheDocument();
});
it("renders the Cost chip for a finite cost", () => {
render(<ResponseMetrics usage={{ ...baseUsage, cost: 0.000063 }} />);
expect(screen.getByText("Cost: $0.000063")).toBeInTheDocument();
});
});

View file

@ -159,7 +159,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
/>
)}
{usage?.cost !== undefined && (
{typeof usage?.cost === "number" && Number.isFinite(usage.cost) && (
<MetricItem
label="Cost"
tooltip="Cost"

View file

@ -466,6 +466,12 @@ describe("chat_completion prompt cache usage", () => {
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() }));
});
});
describe("chat_completion response cache", () => {

View file

@ -245,7 +245,10 @@ export async function makeOpenAIChatCompletionRequest(
// Extract cost from usage object if available
if (chunkWithUsage.usage.cost !== undefined && chunkWithUsage.usage.cost !== null) {
usageData.cost = parseFloat(chunkWithUsage.usage.cost);
const parsedCost = parseFloat(chunkWithUsage.usage.cost);
if (Number.isFinite(parsedCost)) {
usageData.cost = parsedCost;
}
}
onUsageData(usageData);

View file

@ -231,6 +231,35 @@ 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 replay MCP output items as events for a non-streaming response", async () => {
mockResponsesCreate.mockReturnValueOnce(
nonStreamingResponse({

View file

@ -312,7 +312,10 @@ export async function makeOpenAIResponsesRequest(
}
if (usage.cost !== undefined && usage.cost !== null) {
usageData.cost = Number(usage.cost);
const parsedCost = Number(usage.cost);
if (Number.isFinite(parsedCost)) {
usageData.cost = parsedCost;
}
}
onUsageData(usageData, mcpToolUsed);