diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx index 1b0194b6478..4ed7d8923ee 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx @@ -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) => { + await user.click(screen.getAllByTestId("run-health-check-btn")[1]); + }, + "a bulk run of the selected model": async (user: ReturnType) => { + 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"]); + }); + }); }); }); diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx index 3c274d2e59b..799c4375077 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx @@ -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 = ({ 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 = ({ 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 = ({ ...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 = ({ } } 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 = ({ 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 = ({ } } 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 = ({ ...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, diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthChecksTableColumns.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthChecksTableColumns.tsx index 85539dc1baa..65d8d40ea9c 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthChecksTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthChecksTableColumns.tsx @@ -60,6 +60,11 @@ const SENTINEL_LABEL_KEYS: Readonly> = { 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 = ( {row.original.health_loading ? t("modelDashboard.healthChecksTableColumns.sentinels.checkInProgress") - : sentinelLabel(row.original.last_check, t)} + : checkTimeLabel(row.original.last_check, t)} ), }, @@ -453,7 +458,7 @@ export const getHealthChecksTableColumns = ( cell: ({ row }) => { const modelId = row.original.model_info?.id ?? ""; const lastSuccess = modelHealthStatuses[modelId]?.lastSuccess || NONE; - return {sentinelLabel(lastSuccess, t)}; + return {checkTimeLabel(lastSuccess, t)}; }, }, { diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index a1d3e498dd4..4c98e8726a5 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -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( + {}} + 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( + {}} + 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" }), + ); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 11ab88b5f8a..62293c0c3a2 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -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(), }); }; diff --git a/ui/litellm-dashboard/src/utils/dataUtils.test.ts b/ui/litellm-dashboard/src/utils/dataUtils.test.ts index 23b425b47e0..c07fb7234c0 100644 --- a/ui/litellm-dashboard/src/utils/dataUtils.test.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.test.ts @@ -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", () => { diff --git a/ui/litellm-dashboard/src/utils/dataUtils.ts b/ui/litellm-dashboard/src/utils/dataUtils.ts index 51f53ed46c3..08c5051d671 100644 --- a/ui/litellm-dashboard/src/utils/dataUtils.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.ts @@ -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 =>