mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(ui): stop parsing formatted dates and spend back into values (#50350)
* fix(ui): stop parsing formatted dates and spend back into values Regenerating a key stored its new created_at as locale text and parsed it back for display, which shows "Invalid Date" on browsers whose date format JavaScript cannot parse. It now stores an ISO timestamp. The model health table stored check times as locale text and sorted by parsing that text, which misorders or drops rows outside US English. It now keeps the raw timestamp and formats it in the table cell, so the shown text is unchanged. getSpendString decided whether spend rounds to zero by stripping commas from its own formatted output. It now compares the number against half of the smallest shown unit, which gives the same output for every value and no longer depends on how the digits are rendered. * fix(ui): keep getSpendString exact at high precision 0.5 / 10 ** decimals drifts by one ulp once 10 ** decimals is no longer exact (past 22 decimals), so a value right at the threshold could flip from "< $..." to a row of zeros. Parsing 5e-(decimals + 1) gives the correctly rounded threshold for every precision.
This commit is contained in:
parent
de0b389205
commit
0057804dca
7 changed files with 250 additions and 34 deletions
|
|
@ -3,7 +3,7 @@ import type { PaginationState } from "@tanstack/react-table";
|
|||
import { act, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useState } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import HealthCheckComponent from "./HealthCheckComponent";
|
||||
|
||||
|
|
@ -65,6 +65,28 @@ const renderHealthCheck = async (props: HarnessProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const numericDateTime: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
};
|
||||
const germanDateTime = new Intl.DateTimeFormat("de-DE", numericDateTime);
|
||||
|
||||
const germanLocaleString = function (this: Date): string {
|
||||
return germanDateTime.format(this);
|
||||
};
|
||||
|
||||
const rowIds = (): string[] =>
|
||||
screen
|
||||
.getAllByRole("row")
|
||||
.slice(1)
|
||||
.map((row) => row.querySelector('[data-testid^="datatable-select-row-"]'))
|
||||
.filter((node): node is Element => node !== null)
|
||||
.map((node) => (node.getAttribute("data-testid") ?? "").replace("datatable-select-row-", ""));
|
||||
|
||||
describe("HealthCheckComponent", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -251,5 +273,90 @@ describe("HealthCheckComponent", () => {
|
|||
expect(screen.queryByText("healthy")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("none")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not count a failed stored check as the last success", async () => {
|
||||
const checkedAt = "2024-01-15T10:05:00Z";
|
||||
mockLatestHealthChecksCall.mockResolvedValue({
|
||||
latest_health_checks: {
|
||||
"id-alpha": { status: "unhealthy", checked_at: checkedAt, error_message: "Connection failed" },
|
||||
},
|
||||
});
|
||||
|
||||
await renderHealthCheck({ modelData: { data: [makeModel("id-alpha")] }, allModelsOnProxy: ["id-alpha"] });
|
||||
|
||||
expect(screen.getAllByText(new Date(checkedAt).toLocaleString())).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("check times on a German-locale browser", () => {
|
||||
const twoModels = { data: [makeModel("id-alpha", "alpha"), makeModel("id-beta", "beta")] };
|
||||
const bothIds = ["id-alpha", "id-beta"];
|
||||
const isoLikeLocaleString = vi.mocked(Date.prototype.toLocaleString).getMockImplementation();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(Date.prototype.toLocaleString).mockImplementation(germanLocaleString);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
if (isoLikeLocaleString) {
|
||||
vi.mocked(Date.prototype.toLocaleString).mockImplementation(isoLikeLocaleString);
|
||||
}
|
||||
});
|
||||
|
||||
it("sorts stored checks most-recent-first and shows them in the browser's format", async () => {
|
||||
const march = "2024-03-02T10:00:00Z";
|
||||
const june = "2024-06-01T10:00:00Z";
|
||||
mockLatestHealthChecksCall.mockResolvedValue({
|
||||
latest_health_checks: {
|
||||
"id-alpha": { status: "healthy", checked_at: march, error_message: null },
|
||||
"id-beta": { status: "healthy", checked_at: june, error_message: null },
|
||||
},
|
||||
});
|
||||
await renderHealthCheck({ modelData: twoModels, allModelsOnProxy: bothIds, rowCount: 2 });
|
||||
const user = userEvent.setup();
|
||||
|
||||
expect(await screen.findAllByText(germanLocaleString.call(new Date(june)))).toHaveLength(2);
|
||||
|
||||
await user.click(screen.getByTestId("sort-header-last_check"));
|
||||
|
||||
expect(rowIds()).toEqual(["id-beta", "id-alpha"]);
|
||||
});
|
||||
|
||||
const runBetaAlone = {
|
||||
"a single-model check": async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
await user.click(screen.getAllByTestId("run-health-check-btn")[1]);
|
||||
},
|
||||
"a bulk run of the selected model": async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
await user.click(screen.getByTestId("datatable-select-row-id-beta"));
|
||||
await user.click(screen.getByTestId("run-health-checks"));
|
||||
},
|
||||
};
|
||||
|
||||
describe.each(Object.entries(runBetaAlone))("after %s", (_trigger, runCheck) => {
|
||||
it.each([
|
||||
["succeeds", () => mockIndividualModelHealthCheckCall.mockResolvedValue({ unhealthy_count: 0 })],
|
||||
["fails", () => mockIndividualModelHealthCheckCall.mockRejectedValue(new Error("upstream down"))],
|
||||
])("that %s, sorts the fresh check above an older stored one", async (_outcome, arrangeCheck) => {
|
||||
const now = new Date("2026-01-20T12:00:00Z");
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(now);
|
||||
arrangeCheck();
|
||||
mockLatestHealthChecksCall.mockResolvedValue({
|
||||
latest_health_checks: {
|
||||
"id-alpha": { status: "healthy", checked_at: "2024-06-01T10:00:00Z", error_message: null },
|
||||
},
|
||||
});
|
||||
await renderHealthCheck({ modelData: twoModels, allModelsOnProxy: bothIds, rowCount: 2 });
|
||||
const user = userEvent.setup();
|
||||
|
||||
await runCheck(user);
|
||||
expect((await screen.findAllByText(germanLocaleString.call(now))).length).toBeGreaterThan(0);
|
||||
|
||||
await user.click(screen.getByTestId("sort-header-last_check"));
|
||||
|
||||
expect(rowIds()).toEqual(["id-beta", "id-alpha"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -131,19 +131,10 @@ const extractMeaningfulError = (error: unknown, t: TFunction = i18n.t.bind(i18n)
|
|||
return truncate(cleaned);
|
||||
};
|
||||
|
||||
const toCheckedAtLabel = (checkedAt: string | null | undefined, fallback: string): string => {
|
||||
if (!checkedAt) {
|
||||
return fallback;
|
||||
}
|
||||
return new Date(checkedAt).toLocaleString();
|
||||
};
|
||||
const checkedAtOr = (checkedAt: string | null | undefined, fallback: string): string => checkedAt || fallback;
|
||||
|
||||
const toLastSuccessLabel = (checkData: LatestHealthCheck, fallback: string): string => {
|
||||
if (checkData.status !== "healthy") {
|
||||
return fallback;
|
||||
}
|
||||
return toCheckedAtLabel(checkData.checked_at, fallback);
|
||||
};
|
||||
const lastSuccessAtOr = (checkData: LatestHealthCheck, fallback: string): string =>
|
||||
checkData.status === "healthy" ? checkedAtOr(checkData.checked_at, fallback) : fallback;
|
||||
|
||||
interface HealthCheckComponentProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -229,8 +220,8 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
|
||||
healthStatusMap[modelId] = {
|
||||
status: checkData.status || "unknown",
|
||||
lastCheck: toCheckedAtLabel(checkData.checked_at, "None"),
|
||||
lastSuccess: toLastSuccessLabel(checkData, "None"),
|
||||
lastCheck: checkedAtOr(checkData.checked_at, "None"),
|
||||
lastSuccess: lastSuccessAtOr(checkData, "None"),
|
||||
loading: false,
|
||||
error: fullError ? extractMeaningfulError(fullError) : undefined,
|
||||
fullError: fullError,
|
||||
|
|
@ -263,7 +254,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
|
||||
try {
|
||||
const response = await individualModelHealthCheckCall(accessToken, modelId);
|
||||
const currentTime = new Date().toLocaleString();
|
||||
const currentTime = new Date().toISOString();
|
||||
|
||||
if (response.unhealthy_count > 0 && response.unhealthy_endpoints && response.unhealthy_endpoints.length > 0) {
|
||||
const rawError =
|
||||
|
|
@ -303,8 +294,8 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
...prev,
|
||||
[modelId]: {
|
||||
status: checkData.status || prev[modelId]?.status || "unknown",
|
||||
lastCheck: toCheckedAtLabel(checkData.checked_at, prev[modelId]?.lastCheck || "None"),
|
||||
lastSuccess: toLastSuccessLabel(checkData, prev[modelId]?.lastSuccess || "None"),
|
||||
lastCheck: checkedAtOr(checkData.checked_at, prev[modelId]?.lastCheck || "None"),
|
||||
lastSuccess: lastSuccessAtOr(checkData, prev[modelId]?.lastSuccess || "None"),
|
||||
loading: false,
|
||||
error: fullError ? extractMeaningfulError(fullError) : prev[modelId]?.error,
|
||||
fullError: fullError || prev[modelId]?.fullError,
|
||||
|
|
@ -314,7 +305,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
}
|
||||
} catch (dbError) {}
|
||||
} catch (error) {
|
||||
const currentTime = new Date().toLocaleString();
|
||||
const currentTime = new Date().toISOString();
|
||||
const rawError = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage = extractMeaningfulError(rawError);
|
||||
setModelHealthStatuses((prev) => ({
|
||||
|
|
@ -361,7 +352,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
try {
|
||||
const response = await individualModelHealthCheckCall(accessToken, modelId);
|
||||
|
||||
const currentTime = new Date().toLocaleString();
|
||||
const currentTime = new Date().toISOString();
|
||||
if (response.unhealthy_count > 0 && response.unhealthy_endpoints && response.unhealthy_endpoints.length > 0) {
|
||||
const rawError =
|
||||
response.unhealthy_endpoints[0]?.error || t("modelDashboard.healthCheckComponent.healthCheckFailed");
|
||||
|
|
@ -391,7 +382,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
}
|
||||
} catch (error) {
|
||||
console.error(`Health check failed for model id ${modelId}:`, error);
|
||||
const currentTime = new Date().toLocaleString();
|
||||
const currentTime = new Date().toISOString();
|
||||
const rawError = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage = extractMeaningfulError(rawError, t);
|
||||
setModelHealthStatuses((prev) => ({
|
||||
|
|
@ -426,8 +417,8 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
...prev,
|
||||
[modelId]: {
|
||||
status: checkData.status || currentStatus?.status || "unknown",
|
||||
lastCheck: toCheckedAtLabel(checkData.checked_at, currentStatus?.lastCheck || "None"),
|
||||
lastSuccess: toLastSuccessLabel(checkData, currentStatus?.lastSuccess || "None"),
|
||||
lastCheck: checkedAtOr(checkData.checked_at, currentStatus?.lastCheck || "None"),
|
||||
lastSuccess: lastSuccessAtOr(checkData, currentStatus?.lastSuccess || "None"),
|
||||
loading: false,
|
||||
error: fullError ? extractMeaningfulError(fullError, t) : currentStatus?.error,
|
||||
fullError: fullError || currentStatus?.fullError,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ const SENTINEL_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
|
|||
const sentinelLabel = (value: string, t: TFunction): string =>
|
||||
Object.hasOwn(SENTINEL_LABEL_KEYS, value) ? t(SENTINEL_LABEL_KEYS[value]) : value;
|
||||
|
||||
const checkTimeLabel = (value: string, t: TFunction): string => {
|
||||
const checkedAt = new Date(value);
|
||||
return Number.isNaN(checkedAt.getTime()) ? sentinelLabel(value, t) : checkedAt.toLocaleString();
|
||||
};
|
||||
|
||||
function HealthStatusBadge({ status }: { status: string }) {
|
||||
const tone = HEALTH_STATUS_TONES[status];
|
||||
if (!tone) {
|
||||
|
|
@ -427,7 +432,7 @@ export const getHealthChecksTableColumns = (
|
|||
<span className="text-sm text-muted-foreground">
|
||||
{row.original.health_loading
|
||||
? t("modelDashboard.healthChecksTableColumns.sentinels.checkInProgress")
|
||||
: sentinelLabel(row.original.last_check, t)}
|
||||
: checkTimeLabel(row.original.last_check, t)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
|
@ -453,7 +458,7 @@ export const getHealthChecksTableColumns = (
|
|||
cell: ({ row }) => {
|
||||
const modelId = row.original.model_info?.id ?? "";
|
||||
const lastSuccess = modelHealthStatuses[modelId]?.lastSuccess || NONE;
|
||||
return <span className="text-sm text-muted-foreground">{sentinelLabel(lastSuccess, t)}</span>;
|
||||
return <span className="text-sm text-muted-foreground">{checkTimeLabel(lastSuccess, t)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
|||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import { keyDeleteCall, keyUpdateCall, teamInfoCall } from "../networking";
|
||||
import { keyDeleteCall, keyUpdateCall, regenerateKeyCall, teamInfoCall } from "../networking";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import KeyInfoView, { needsLifetimeSpendBackfill } from "./key_info_view";
|
||||
|
||||
|
|
@ -92,6 +92,7 @@ vi.mock("../networking", () => ({
|
|||
serverRootPath: "",
|
||||
keyDeleteCall: vi.fn().mockResolvedValue({}),
|
||||
keyUpdateCall: vi.fn().mockResolvedValue({}),
|
||||
regenerateKeyCall: vi.fn().mockResolvedValue({ key: "sk-regenerated", token: "regenerated-token" }),
|
||||
teamInfoCall: vi.fn(),
|
||||
getPolicyInfoWithGuardrails: vi.fn().mockResolvedValue({
|
||||
resolved_guardrails: ["guardrail-1", "guardrail-2"],
|
||||
|
|
@ -1481,4 +1482,86 @@ describe("KeyInfoView", () => {
|
|||
invalidateSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("regenerate on a German-locale browser", () => {
|
||||
const isoLikeLocaleString = vi.mocked(Date.prototype.toLocaleString).getMockImplementation();
|
||||
const numericDateTime: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
};
|
||||
const germanDateTime = new Intl.DateTimeFormat("de-DE", numericDateTime);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-01-20T12:00:00Z"));
|
||||
vi.mocked(Date.prototype.toLocaleString).mockImplementation(function (this: Date) {
|
||||
return germanDateTime.format(this);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
if (isoLikeLocaleString) {
|
||||
vi.mocked(Date.prototype.toLocaleString).mockImplementation(isoLikeLocaleString);
|
||||
}
|
||||
});
|
||||
|
||||
it("shows the regeneration time as the key's created time", async () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: "proxy-admin-user",
|
||||
userRole: "proxy_admin",
|
||||
});
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onClose={() => {}}
|
||||
keyId="test-key-id"
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Regenerate Key" }));
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Regenerate" }));
|
||||
await waitFor(() => expect(regenerateKeyCall).toHaveBeenCalled());
|
||||
await userEvent.keyboard("{Escape}");
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Settings" }));
|
||||
|
||||
const lastRegenerated = within(screen.getByText("Last Regenerated").parentElement!).getByText(/ at /);
|
||||
expect(lastRegenerated).toHaveTextContent("2026");
|
||||
expect(screen.getByText("Created").nextElementSibling).toHaveTextContent(lastRegenerated.textContent!);
|
||||
});
|
||||
|
||||
it("hands the parent the regeneration time as an ISO timestamp", async () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: "proxy-admin-user",
|
||||
userRole: "proxy_admin",
|
||||
});
|
||||
const onKeyDataUpdate = vi.fn();
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onClose={() => {}}
|
||||
keyId="test-key-id"
|
||||
onKeyDataUpdate={onKeyDataUpdate}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Regenerate Key" }));
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Regenerate" }));
|
||||
await waitFor(() => expect(regenerateKeyCall).toHaveBeenCalled());
|
||||
await userEvent.keyboard("{Escape}");
|
||||
|
||||
expect(onKeyDataUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ token: "regenerated-token", created_at: "2026-01-20T12:00:00.000Z" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ export default function KeyInfoView({
|
|||
...prevData,
|
||||
...updatedKeyData, // This should include the new token (key-id)
|
||||
// Update the created_at to show when it was regenerated
|
||||
created_at: regeneratedAt.toLocaleString(),
|
||||
created_at: regeneratedAt.toISOString(),
|
||||
};
|
||||
return newData;
|
||||
});
|
||||
|
|
@ -463,7 +463,7 @@ export default function KeyInfoView({
|
|||
|
||||
setKeyDataUpdateHeldUntilModalClose({
|
||||
...updatedKeyData,
|
||||
created_at: regeneratedAt.toLocaleString(),
|
||||
created_at: regeneratedAt.toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ import {
|
|||
import { toast } from "@/lib/toast";
|
||||
const mockToast = vi.mocked(toast);
|
||||
|
||||
const arabicDigitsLocaleString = function (
|
||||
this: number,
|
||||
_locales?: Intl.LocalesArgument,
|
||||
options?: Intl.NumberFormatOptions,
|
||||
): string {
|
||||
return new Intl.NumberFormat("ar-EG", options).format(this);
|
||||
};
|
||||
|
||||
describe("dataUtils", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -118,6 +126,30 @@ describe("dataUtils", () => {
|
|||
expect(getSpendString(0.01234, 3)).toBe("$0.012");
|
||||
expect(getSpendString(999.9999, 1)).toBe("$1,000.0");
|
||||
});
|
||||
|
||||
it("rounds a spend of exactly half the smallest unit up instead of reporting it below the threshold", () => {
|
||||
expect(getSpendString(0.0000005, 6)).toBe("$0.000001");
|
||||
expect(getSpendString(-0.0000005, 6)).toBe("$-0.000001");
|
||||
});
|
||||
|
||||
it("reports spend just under half the smallest unit as below the threshold", () => {
|
||||
expect(getSpendString(0.00000045, 6)).toBe("< $0.000001");
|
||||
expect(getSpendString(4.9999999999999996e-26, 25)).toBe("< $0.0000000000000000000000001");
|
||||
});
|
||||
|
||||
describe("when numbers render with non-Latin digits", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Number.prototype, "toLocaleString").mockImplementation(arabicDigitsLocaleString);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(Number.prototype.toLocaleString).mockRestore();
|
||||
});
|
||||
|
||||
it("still reports spend that rounds to zero as below the threshold", () => {
|
||||
expect(getSpendString(0.0000004, 6)).toBe("< $0.000001");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPerSecondCost", () => {
|
||||
|
|
|
|||
|
|
@ -53,15 +53,13 @@ export const getSpendString = (value: number | null | undefined, decimals: numbe
|
|||
return "-";
|
||||
}
|
||||
|
||||
const formatted = formatNumberWithCommas(value, decimals, false, false);
|
||||
const numericFormatted = Number(formatted.replace(/,/g, ""));
|
||||
|
||||
if (numericFormatted === 0) {
|
||||
const halfSmallestUnit = Number(`5e-${decimals + 1}`);
|
||||
if (Math.abs(value) < halfSmallestUnit) {
|
||||
const threshold = (1 / 10 ** decimals).toFixed(decimals);
|
||||
return `< $${threshold}`;
|
||||
}
|
||||
|
||||
return `$${formatted}`;
|
||||
return `$${formatNumberWithCommas(value, decimals, false, false)}`;
|
||||
};
|
||||
|
||||
export const formatPerSecondCost = (cost: number): string =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue