mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-27 06:13:07 +00:00
Merge 258e185135 into 8d6b8dab3d
This commit is contained in:
commit
9c837b23dd
6 changed files with 196 additions and 7 deletions
|
|
@ -0,0 +1,121 @@
|
|||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { EMPTY_DAILY_ACTIVITY_RESPONSE } from "@/components/UsagePage/dailyActivityApi";
|
||||
import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils";
|
||||
import type { AutoRouterBenchmarksResponse } from "./autoRouterBenchmarks";
|
||||
import CostOptimizationView from "./CostOptimizationView";
|
||||
|
||||
const emptyBucket = { turns: 0, hits: 0, hit_rate_pct: 0 };
|
||||
const emptyBenchmarks: AutoRouterBenchmarksResponse = {
|
||||
start_date: "2026-09-14",
|
||||
end_date: "2026-10-14",
|
||||
routers_in_scope: 0,
|
||||
groups: [],
|
||||
totals: {
|
||||
sessions: 0,
|
||||
turns: 0,
|
||||
avg_turns_per_session: 0,
|
||||
avg_session_seconds: 0,
|
||||
avg_tokens_per_session: 0,
|
||||
spend: 0,
|
||||
savings_estimated_turns: 0,
|
||||
savings_estimated_actual_spend: 0,
|
||||
savings_estimated_classifier_cost: 0,
|
||||
classifier_cost: 0,
|
||||
saved_spend: 0,
|
||||
baseline_spend: 0,
|
||||
saved_pct: 0,
|
||||
cache: {
|
||||
coverage_pct: 0,
|
||||
hit_rate_pct: 0,
|
||||
same_model: emptyBucket,
|
||||
first_visit: emptyBucket,
|
||||
return_to_tier: emptyBucket,
|
||||
unordered_turns: 0,
|
||||
return_misses_expired: 0,
|
||||
return_misses_within_ttl: 0,
|
||||
return_misses_unknown: 0,
|
||||
ttl_5m_turns: 0,
|
||||
ttl_1h_turns: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const requestUrl = (input: Parameters<typeof fetch>[0]): URL =>
|
||||
new URL(input instanceof Request ? input.url : String(input), "http://localhost:4000");
|
||||
|
||||
describe("CostOptimizationView shared date range", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-10-14T12:00:00"));
|
||||
vi.stubGlobal("requestIdleCallback", (callback: IdleRequestCallback) => {
|
||||
callback({ didTimeout: false, timeRemaining: () => 50 });
|
||||
return 0;
|
||||
});
|
||||
const payload = btoa(JSON.stringify({ key: "sk-test", user_id: "u1", user_role: "proxy_admin" }));
|
||||
document.cookie = `token=e30.${payload}.test; path=/`;
|
||||
testQueryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.cookie = "token=; max-age=0; path=/";
|
||||
testQueryClient.clear();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("shares the initial seven days and a custom wider range across the three date pickers", async () => {
|
||||
const mockFetch = vi.fn<typeof fetch>().mockImplementation(async (input) => {
|
||||
const path = requestUrl(input).pathname;
|
||||
const responses: Readonly<Record<string, unknown>> = {
|
||||
"/user/daily/activity/aggregated": EMPTY_DAILY_ACTIVITY_RESPONSE,
|
||||
"/litellm/.well-known/litellm-ui-config": { admin_ui_disabled: false },
|
||||
"/v1/tool/spend": { by_tool: [], daily: [] },
|
||||
"/config/list": [],
|
||||
"/v2/model/info": { data: [] },
|
||||
"/auto_router/benchmarks": emptyBenchmarks,
|
||||
"/user/daily/activity/aggregated/cache_leakage_keys": { api_keys: [] },
|
||||
"/cost_optimization/prompt_caching/requests": { requests: [], next_cursor: null },
|
||||
};
|
||||
const data = responses[path] ?? [];
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
renderWithProviders(<CostOptimizationView accessToken="sk-test" userId="u1" userRole="proxy_admin" />);
|
||||
|
||||
const activityRequests = () =>
|
||||
mockFetch.mock.calls
|
||||
.map(([input]) => requestUrl(input))
|
||||
.filter((url) => url.pathname === "/user/daily/activity/aggregated");
|
||||
await waitFor(() => expect(activityRequests()).toHaveLength(1));
|
||||
expect(activityRequests()[0].searchParams.get("start_date")).toBe("2026-10-08");
|
||||
expect(activityRequests()[0].searchParams.get("end_date")).toBe("2026-10-14");
|
||||
|
||||
const initialRange = /8 Oct, 00:00 - 14 Oct, 23:59/;
|
||||
expect(screen.getByRole("button", { name: initialRange })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Prompt Caching" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: initialRange }));
|
||||
fireEvent.change(screen.getByDisplayValue("2026-10-08"), { target: { value: "2026-09-14" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apply" }));
|
||||
|
||||
await waitFor(() => expect(activityRequests().at(-1)?.searchParams.get("start_date")).toBe("2026-09-14"));
|
||||
expect(activityRequests().at(-1)?.searchParams.get("end_date")).toBe("2026-10-14");
|
||||
const selectedRange = /14 Sep, 00:00 - 14 Oct, 23:59/;
|
||||
expect(screen.getByRole("button", { name: selectedRange })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Auto-Router" }));
|
||||
expect(screen.getByRole("button", { name: selectedRange })).toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ url: expect.stringContaining("/auto_router/benchmarks?") }),
|
||||
),
|
||||
);
|
||||
const benchmarkUrl = mockFetch.mock.calls
|
||||
.map(([input]) => requestUrl(input))
|
||||
.find((url) => url.pathname === "/auto_router/benchmarks");
|
||||
expect(benchmarkUrl?.searchParams.get("start_date")).toBe("2026-09-14");
|
||||
expect(benchmarkUrl?.searchParams.get("end_date")).toBe("2026-10-14");
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Overall" }));
|
||||
expect(screen.getByRole("button", { name: selectedRange })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { costOptimizationDateRange } from "./costOptimizationDateRange";
|
||||
|
||||
describe("costOptimizationDateRange", () => {
|
||||
it.each([
|
||||
["2026-10-14", "2026-10-08", "12:34:56"],
|
||||
["2026-03-02", "2026-02-24", "12:34:56"],
|
||||
["2028-03-02", "2028-02-25", "12:34:56"],
|
||||
["2026-01-02", "2025-12-27", "12:34:56"],
|
||||
["2026-03-09", "2026-03-03", "00:30:00"],
|
||||
["2026-11-02", "2026-10-27", "23:30:00"],
|
||||
])("includes exactly seven local calendar days ending on %s", (today, firstDay, time) => {
|
||||
const now = new Date(`${today}T${time}`);
|
||||
|
||||
expect(costOptimizationDateRange(now)).toEqual({
|
||||
from: new Date(`${firstDay}T00:00:00`),
|
||||
to: new Date(`${today}T23:59:59.999`),
|
||||
});
|
||||
expect(now).toEqual(new Date(`${today}T${time}`));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { endOfDay, startOfDay, subDays } from "date-fns";
|
||||
|
||||
export const costOptimizationDateRange = (now: Date = new Date()): { from: Date; to: Date } => ({
|
||||
from: startOfDay(subDays(now, 6)),
|
||||
to: endOfDay(now),
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useScopedDailyActivityRange } from "./useDailyActivityRange";
|
||||
import { useDailyActivityRange, useScopedDailyActivityRange } from "./useDailyActivityRange";
|
||||
|
||||
// The sibling unit test mocks networking, so it pins the positional args array against itself and
|
||||
// would still pass if the array and the two networking signatures drifted apart. Nothing else runs
|
||||
|
|
@ -13,6 +13,38 @@ describe("useScopedDailyActivityRange wiring", () => {
|
|||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("requests seven days including today on mount and preserves a wider selected range", async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-03-09T12:00:00"));
|
||||
const mockFetch = vi.fn<typeof fetch>().mockImplementation(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ results: [], metadata: {} }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
global.fetch = mockFetch;
|
||||
const { result } = renderHook(() => useDailyActivityRange("sk-token", "u1", "proxy_admin"));
|
||||
|
||||
await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1));
|
||||
const initialUrl = new URL(String(mockFetch.mock.calls[0][0]));
|
||||
expect(initialUrl.pathname).toBe("/user/daily/activity/aggregated");
|
||||
expect(initialUrl.searchParams.get("start_date")).toBe("2026-03-03");
|
||||
expect(initialUrl.searchParams.get("end_date")).toBe("2026-03-09");
|
||||
expect(initialUrl.searchParams.get("include_current_utc_day")).toBe("true");
|
||||
expect(initialUrl.searchParams.get("timezone")).toBe(String(new Date().getTimezoneOffset()));
|
||||
|
||||
const selectedRange = { from: new Date(2026, 0, 1), to: new Date(2026, 2, 9, 23, 59, 59, 999) };
|
||||
act(() => result.current.onDateChange(selectedRange));
|
||||
|
||||
await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2));
|
||||
const selectedUrl = new URL(String(mockFetch.mock.calls[1][0]));
|
||||
expect(selectedUrl.searchParams.get("start_date")).toBe("2026-01-01");
|
||||
expect(selectedUrl.searchParams.get("end_date")).toBe("2026-03-09");
|
||||
expect(result.current.dateValue).toEqual(selectedRange);
|
||||
});
|
||||
|
||||
it("lands each scope field on its own query param through the real networking callers", async () => {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ describe("useDailyActivityRange", () => {
|
|||
|
||||
expect(result.current.dateValue.from).toBeInstanceOf(Date);
|
||||
expect(result.current.dateValue.to).toBeInstanceOf(Date);
|
||||
expect(result.current.dateValue.to!.getTime() - result.current.dateValue.from!.getTime()).toBe(
|
||||
30 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
expect(mockUseAggregatedDailyActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { DailyData } from "@/components/UsagePage/types";
|
||||
import { spendScopeUserId } from "@/utils/roles";
|
||||
import { useAggregatedDailyActivity } from "@/app/(dashboard)/usage/_components/hooks/useAggregatedDailyActivity";
|
||||
import { costOptimizationDateRange } from "./costOptimizationDateRange";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
|
|
@ -38,10 +39,14 @@ export interface DailyActivityRange {
|
|||
|
||||
export type ActivityDateRange = Pick<DailyActivityRange, "dateValue" | "onDateChange">;
|
||||
|
||||
export const useActivityDateRange = (): ActivityDateRange => {
|
||||
const initialFrom = useMemo(() => new Date(new Date().getTime() - THIRTY_DAYS_MS), []);
|
||||
const initialTo = useMemo(() => new Date(), []);
|
||||
const [dateValue, setDateValue] = useState<DateRange>({ from: initialFrom, to: initialTo });
|
||||
export const useActivityDateRange = (initialRange?: () => DateRange): ActivityDateRange => {
|
||||
const [dateValue, setDateValue] = useState<DateRange>(
|
||||
initialRange ??
|
||||
(() => {
|
||||
const now = new Date();
|
||||
return { from: new Date(now.getTime() - THIRTY_DAYS_MS), to: now };
|
||||
}),
|
||||
);
|
||||
return { dateValue, onDateChange: setDateValue };
|
||||
};
|
||||
|
||||
|
|
@ -96,6 +101,6 @@ export const useDailyActivityRange = (
|
|||
userId: string | null,
|
||||
userRole: string,
|
||||
): DailyActivityRange => {
|
||||
const dateRange = useActivityDateRange();
|
||||
const dateRange = useActivityDateRange(costOptimizationDateRange);
|
||||
return useScopedDailyActivityRange(accessToken, { userId: spendScopeUserId(userRole, userId) }, dateRange);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue