fix(usage): reduce price input step to 0.0001 for sub-cent precision (#2793)

The step was 0.01, preventing input of prices like DeepSeek's cache read
cost ($0.0028/million tokens). Extract step value to a constant and apply
to all four price fields.

Closes #2503
This commit is contained in:
Roger Deng
2026-05-18 11:38:32 +08:00
committed by GitHub
Unverified
parent 5ae9c2605d
commit 6e8ee094f1
2 changed files with 99 additions and 4 deletions
+6 -4
View File
@@ -16,6 +16,8 @@ interface PricingEditModalProps {
onClose: () => void;
}
const PRICE_INPUT_STEP = "0.0001";
export function PricingEditModal({
open,
model,
@@ -151,7 +153,7 @@ export function PricingEditModal({
<Input
id="inputCost"
type="number"
step="0.01"
step={PRICE_INPUT_STEP}
min="0"
value={formData.inputCost}
onChange={(e) =>
@@ -168,7 +170,7 @@ export function PricingEditModal({
<Input
id="outputCost"
type="number"
step="0.01"
step={PRICE_INPUT_STEP}
min="0"
value={formData.outputCost}
onChange={(e) =>
@@ -188,7 +190,7 @@ export function PricingEditModal({
<Input
id="cacheReadCost"
type="number"
step="0.01"
step={PRICE_INPUT_STEP}
min="0"
value={formData.cacheReadCost}
onChange={(e) =>
@@ -208,7 +210,7 @@ export function PricingEditModal({
<Input
id="cacheCreationCost"
type="number"
step="0.01"
step={PRICE_INPUT_STEP}
min="0"
value={formData.cacheCreationCost}
onChange={(e) =>
@@ -0,0 +1,93 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { PricingEditModal } from "@/components/usage/PricingEditModal";
import type { ModelPricing } from "@/types/usage";
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, options?: string | { defaultValue?: string }) =>
typeof options === "string" ? options : options?.defaultValue ?? key,
}),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
vi.mock("@/lib/query/usage", () => ({
useUpdateModelPricing: () => ({
mutateAsync: vi.fn(),
isPending: false,
}),
}));
vi.mock("@/components/common/FullScreenPanel", () => ({
FullScreenPanel: ({
children,
footer,
}: {
children: React.ReactNode;
footer?: React.ReactNode;
}) => (
<div>
{children}
{footer}
</div>
),
}));
const model: ModelPricing = {
modelId: "deepseek-v4",
displayName: "DeepSeek V4",
inputCostPerMillion: "1",
outputCostPerMillion: "3",
cacheReadCostPerMillion: "0.0028",
cacheCreationCostPerMillion: "0",
};
const PRICE_FIELDS = [
{ id: "inputCost", label: "输入成本" },
{ id: "outputCost", label: "输出成本" },
{ id: "cacheReadCost", label: "缓存读取成本" },
{ id: "cacheCreationCost", label: "缓存写入成本" },
] as const;
describe("PricingEditModal", () => {
it("all price inputs have step=0.0001", () => {
render(<PricingEditModal open model={model} onClose={() => {}} />);
for (const { id } of PRICE_FIELDS) {
const input = screen.getByLabelText(/每百万 tokens/ as unknown as string, {
selector: `#${id}`,
}) as HTMLInputElement;
expect(input).toHaveAttribute("step", "0.0001");
}
});
it("accepts precise cache read cost like 0.0028", () => {
render(<PricingEditModal open model={model} onClose={() => {}} />);
const cacheReadInput = document.getElementById(
"cacheReadCost",
) as HTMLInputElement;
expect(cacheReadInput.value).toBe("0.0028");
expect(cacheReadInput.checkValidity()).toBe(true);
});
it("allows user to input sub-cent prices via change event", () => {
render(
<PricingEditModal open model={model} onClose={() => {}} isNew />,
);
const cacheReadInput = document.getElementById(
"cacheReadCost",
) as HTMLInputElement;
fireEvent.change(cacheReadInput, { target: { value: "0.0015" } });
expect(cacheReadInput.value).toBe("0.0015");
});
});