mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat(ui): add key-scoped auto-router usage tab
GET /auto_router/benchmarks takes an optional api_key filter, applied in the rollup aggregate on the primary key's leading column. Proxy admins get a separate Auto-router usage tab on key detail pages with spend, baseline, savings, tier routing, cache metrics and the existing router selector
This commit is contained in:
parent
09e9fd5f60
commit
b36d252b26
15 changed files with 271 additions and 17 deletions
|
|
@ -37,7 +37,9 @@ CACHE_TTL_1H_SECONDS: Final = 3600
|
|||
AUTOROUTER_BENCHMARKS_SQL: Final = """
|
||||
WITH windowed AS (
|
||||
SELECT * FROM "LiteLLM_AutoRouterSession"
|
||||
WHERE last_turn_at >= $1::timestamp AND first_turn_at < $2::timestamp
|
||||
WHERE last_turn_at >= $1::timestamp
|
||||
AND first_turn_at < $2::timestamp
|
||||
AND ($3::text IS NULL OR api_key = $3::text)
|
||||
),
|
||||
tier_maps AS (
|
||||
SELECT router_name, router_type, jsonb_object_agg(tier, tier_turns) AS tier_turns
|
||||
|
|
|
|||
|
|
@ -645,6 +645,7 @@ async def get_auto_router_benchmarks(
|
|||
str | None, Query(description="YYYY-MM-DD UTC, inclusive (defaults to 30 days before end_date)")
|
||||
] = None,
|
||||
end_date: Annotated[str | None, Query(description="YYYY-MM-DD UTC, inclusive (defaults to today)")] = None,
|
||||
api_key: Annotated[str | None, Query(description="Filter to one virtual key token hash")] = None,
|
||||
) -> AutoRouterBenchmarksResponse:
|
||||
"""
|
||||
Benchmarks for the auto-router dashboard: session shape, savings against the configured
|
||||
|
|
@ -681,6 +682,7 @@ async def get_auto_router_benchmarks(
|
|||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
start_day.isoformat(),
|
||||
(end_day + timedelta(days=1)).isoformat(),
|
||||
api_key,
|
||||
)
|
||||
rows: Final = _SESSION_AGG_ROWS.validate_python(raw_rows or ())
|
||||
groups: Final = (
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ async def test_the_benchmarks_aggregate_reads_only_overlapping_sessions(db):
|
|||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
None,
|
||||
)
|
||||
matching = [row for row in rows if row["router_name"] == router]
|
||||
assert len(matching) == 1
|
||||
|
|
@ -181,6 +182,33 @@ async def test_the_benchmarks_aggregate_reads_only_overlapping_sessions(db):
|
|||
assert grouped["session_seconds"] == pytest.approx(60.0)
|
||||
|
||||
|
||||
async def test_the_benchmarks_aggregate_can_filter_to_one_key(db):
|
||||
router = f"r-{uuid.uuid4()}"
|
||||
first_key = f"k-{uuid.uuid4()}"
|
||||
second_key = f"k-{uuid.uuid4()}"
|
||||
await _turn(db, first_key, "A", T0, session_id=f"s-{uuid.uuid4()}", router=router, saved=0.5)
|
||||
await _turn(db, second_key, "A", T0, session_id=f"s-{uuid.uuid4()}", router=router, saved=9.0)
|
||||
|
||||
rows = await db.query_raw(
|
||||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
first_key,
|
||||
)
|
||||
matching = [row for row in rows if row["router_name"] == router]
|
||||
assert len(matching) == 1
|
||||
assert matching[0]["sessions"] == 1
|
||||
assert matching[0]["saved_spend"] == pytest.approx(0.5)
|
||||
|
||||
unknown_key_rows = await db.query_raw(
|
||||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
f"k-{uuid.uuid4()}",
|
||||
)
|
||||
assert [row for row in unknown_key_rows if row["router_name"] == router] == []
|
||||
|
||||
|
||||
async def test_a_reconfigured_alias_reports_each_router_type_as_its_own_group(db):
|
||||
key = f"k-{uuid.uuid4()}"
|
||||
router = f"r-{uuid.uuid4()}"
|
||||
|
|
@ -191,6 +219,7 @@ async def test_a_reconfigured_alias_reports_each_router_type_as_its_own_group(db
|
|||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
None,
|
||||
)
|
||||
matching = sorted(
|
||||
(row for row in rows if row["router_name"] == router),
|
||||
|
|
@ -253,6 +282,7 @@ async def test_the_benchmarks_aggregate_sums_tier_turns_across_sessions(db):
|
|||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
None,
|
||||
)
|
||||
grouped = next(row for row in rows if row["router_name"] == router)
|
||||
assert grouped["tier_turns"] == {"simple": 2, "complex": 1}
|
||||
|
|
@ -280,6 +310,7 @@ async def test_tier_maps_stay_separate_per_router_type_on_a_reconfigured_alias(d
|
|||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
None,
|
||||
)
|
||||
by_type = {row["router_type"]: row["tier_turns"] for row in rows if row["router_name"] == router}
|
||||
assert by_type == {"complexity": {"medium": 1}, "quality": {"2": 1}}
|
||||
|
|
@ -294,6 +325,7 @@ async def test_a_window_with_no_tiered_turns_aggregates_to_an_empty_map(db):
|
|||
AUTOROUTER_BENCHMARKS_SQL,
|
||||
(T0 - timedelta(days=1)).isoformat(),
|
||||
(T0 + timedelta(days=1)).isoformat(),
|
||||
None,
|
||||
)
|
||||
grouped = next(row for row in rows if row["router_name"] == router)
|
||||
assert grouped["tier_turns"] == {}
|
||||
|
|
|
|||
|
|
@ -489,6 +489,7 @@ class TestAutoRouterBenchmarks:
|
|||
monkeypatch: pytest.MonkeyPatch,
|
||||
rows: Sequence[Mapping[str, object]],
|
||||
model_list: Sequence[object],
|
||||
api_key: str | None = None,
|
||||
) -> AutoRouterBenchmarksResponse:
|
||||
from litellm.proxy import proxy_server
|
||||
from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_benchmarks
|
||||
|
|
@ -503,6 +504,7 @@ class TestAutoRouterBenchmarks:
|
|||
user_api_key_dict=ADMIN,
|
||||
start_date="2026-07-01",
|
||||
end_date="2026-08-01",
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
ROW = _SessionAggRow(
|
||||
|
|
@ -652,8 +654,9 @@ class TestAutoRouterBenchmarks:
|
|||
user_api_key_dict=ADMIN,
|
||||
start_date="2026-07-01",
|
||||
end_date="2026-08-01",
|
||||
api_key="key-hash",
|
||||
)
|
||||
assert captured["params"] == ("2026-07-01T00:00:00", "2026-08-02T00:00:00")
|
||||
assert captured["params"] == ("2026-07-01T00:00:00", "2026-08-02T00:00:00", "key-hash")
|
||||
assert response.routers_in_scope == 1
|
||||
assert response.groups[0].router_name == "live-auto"
|
||||
assert response.groups[0].saved_pct == response.totals.saved_pct == 75.0
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ vi.mock("@/components/shared/advanced_date_picker", () => ({
|
|||
|
||||
import { useAutoRouters } from "@/app/(dashboard)/hooks/models/useModels";
|
||||
|
||||
import AutoRouterBenchmarksTab from "./AutoRouterBenchmarksTab";
|
||||
import AutoRouterBenchmarksTab, { AutoRouterUsageView } from "./AutoRouterBenchmarksTab";
|
||||
import type {
|
||||
AutoRouterBenchmarkGroup,
|
||||
AutoRouterBenchmarksResponse,
|
||||
|
|
@ -359,13 +359,37 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
mockHook({ data: response([group()]) });
|
||||
const { dateValue, onDateChange } = renderTab();
|
||||
|
||||
expect(vi.mocked(useAutoRouterBenchmarks)).toHaveBeenCalledWith("sk-test", dateValue);
|
||||
expect(vi.mocked(useAutoRouterBenchmarks)).toHaveBeenCalledWith("sk-test", dateValue, undefined);
|
||||
expect(screen.getByText("Jul 6 – Aug 5 (UTC)")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId("date-picker"));
|
||||
expect(onDateChange).toHaveBeenCalledWith({ from: new Date(2026, 7, 1), to: new Date(2026, 7, 5) });
|
||||
});
|
||||
|
||||
it("scopes the query to one key when the usage view is mounted for a key", () => {
|
||||
mockHook({ data: response([group()]) });
|
||||
const dateValue = { from: new Date(2026, 6, 6), to: new Date(2026, 7, 5) };
|
||||
const activity = {
|
||||
dateValue,
|
||||
onDateChange: vi.fn(),
|
||||
results: [],
|
||||
loading: false,
|
||||
isFetchingMore: false,
|
||||
progress: { currentPage: 1, totalPages: 1 },
|
||||
cancelled: false,
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<AutoRouterUsageView accessToken="sk-test" activity={activity} apiKey="key-hash-1" />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(vi.mocked(useAutoRouterBenchmarks)).toHaveBeenCalledWith("sk-test", dateValue, "key-hash-1");
|
||||
expect(screen.getByText("Total estimated savings")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "Shadow Evals" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows usage by default and mounts shadow evals only when its sub-tab is selected", () => {
|
||||
mockHook({ data: response([group()]) });
|
||||
renderTab();
|
||||
|
|
|
|||
|
|
@ -273,12 +273,13 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({ isPending, error, data,
|
|||
|
||||
interface AutoRouterBenchmarksTabProps {
|
||||
accessToken: string | null;
|
||||
activity: DailyActivityRange;
|
||||
activity: Pick<DailyActivityRange, "dateValue" | "onDateChange">;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
const UsageView: React.FC<AutoRouterBenchmarksTabProps> = ({ accessToken, activity }) => {
|
||||
export const AutoRouterUsageView: React.FC<AutoRouterBenchmarksTabProps> = ({ accessToken, activity, apiKey }) => {
|
||||
const { dateValue, onDateChange } = activity;
|
||||
const { data, isPending, error } = useAutoRouterBenchmarks(accessToken, dateValue);
|
||||
const { data, isPending, error } = useAutoRouterBenchmarks(accessToken, dateValue, apiKey);
|
||||
const [selectedKey, setSelectedKey] = useState<string>(ALL_ROUTERS);
|
||||
const { data: autoRouters } = useAutoRouters();
|
||||
|
||||
|
|
@ -347,7 +348,7 @@ const AutoRouterBenchmarksTab: React.FC<AutoRouterBenchmarksTabProps> = ({ acces
|
|||
</TabsList>
|
||||
|
||||
<TabsContent value="usage" keepMounted={visitedTabs.includes("usage")}>
|
||||
<UsageView accessToken={accessToken} activity={activity} />
|
||||
<AutoRouterUsageView accessToken={accessToken} activity={activity} />
|
||||
</TabsContent>
|
||||
<TabsContent value="shadow-evals" keepMounted={visitedTabs.includes("shadow-evals")}>
|
||||
<ShadowEvalSection />
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { describe, expect, it, vi } from "vitest";
|
|||
vi.mock("@/components/networking", () => ({ formatDate: vi.fn() }));
|
||||
vi.mock("@/lib/http/api", () => ({ $api: { useQuery: vi.fn() } }));
|
||||
|
||||
import { benchmarksWindow } from "./useAutoRouterBenchmarks";
|
||||
import { $api } from "@/lib/http/api";
|
||||
|
||||
import { benchmarksWindow, useAutoRouterBenchmarks } from "./useAutoRouterBenchmarks";
|
||||
|
||||
const localDay =
|
||||
(offsetHours: number) =>
|
||||
|
|
@ -44,3 +46,30 @@ describe("benchmarksWindow", () => {
|
|||
expect(benchmarksWindow({ to: now }, now, pacific)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useAutoRouterBenchmarks", () => {
|
||||
const range = { from: new Date("2026-07-06T19:00:00Z"), to: new Date("2026-08-05T19:00:00Z") };
|
||||
|
||||
it("forwards the key hash as the endpoint's api_key filter", () => {
|
||||
useAutoRouterBenchmarks("sk-test", range, "key-hash-1");
|
||||
|
||||
const [, path, init] = vi.mocked($api.useQuery).mock.calls.at(-1) as unknown as [
|
||||
string,
|
||||
string,
|
||||
{ params: { query: Record<string, string | undefined> } },
|
||||
];
|
||||
expect(path).toBe("/auto_router/benchmarks");
|
||||
expect(init.params.query.api_key).toBe("key-hash-1");
|
||||
});
|
||||
|
||||
it("leaves the read deployment-wide when no key is given", () => {
|
||||
useAutoRouterBenchmarks("sk-test", range);
|
||||
|
||||
const [, , init] = vi.mocked($api.useQuery).mock.calls.at(-1) as unknown as [
|
||||
string,
|
||||
string,
|
||||
{ params: { query: Record<string, string | undefined> } },
|
||||
];
|
||||
expect(init.params.query.api_key).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ export const benchmarksWindow = (
|
|||
};
|
||||
};
|
||||
|
||||
export const useAutoRouterBenchmarks = (accessToken: string | null, range: DateRange) =>
|
||||
export const useAutoRouterBenchmarks = (accessToken: string | null, range: DateRange, apiKey?: string) =>
|
||||
$api.useQuery(
|
||||
"get",
|
||||
"/auto_router/benchmarks",
|
||||
{ params: { query: benchmarksWindow(range, new Date()) } },
|
||||
{ params: { query: { ...benchmarksWindow(range, new Date()), api_key: apiKey } } },
|
||||
{ enabled: Boolean(accessToken && range.from && range.to), retry: false },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,11 +25,19 @@ vi.mock("@/components/networking", () => ({
|
|||
}));
|
||||
|
||||
import { userDailyActivityAggregatedCall } from "@/components/networking";
|
||||
import { useDailyActivityRange } from "./useDailyActivityRange";
|
||||
import { useActivityDateRange, useDailyActivityRange } from "./useDailyActivityRange";
|
||||
|
||||
const argsOfLastCall = () => mockUsePaginatedDailyActivity.mock.calls.at(-1)?.[0].args as unknown[];
|
||||
|
||||
describe("useDailyActivityRange", () => {
|
||||
it("offers date-range state without starting a daily-activity query", () => {
|
||||
const { result } = renderHook(() => useActivityDateRange());
|
||||
|
||||
expect(result.current.dateValue.from).toBeInstanceOf(Date);
|
||||
expect(result.current.dateValue.to).toBeInstanceOf(Date);
|
||||
expect(mockUsePaginatedDailyActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("queries every user's activity for an admin", () => {
|
||||
renderHook(() => useDailyActivityRange("test-token", "u1", "proxy_admin"));
|
||||
|
||||
|
|
|
|||
|
|
@ -37,13 +37,18 @@ export interface DailyActivityScope {
|
|||
apiKey?: string | null;
|
||||
}
|
||||
|
||||
export const useActivityDateRange = (): Pick<DailyActivityRange, "dateValue" | "onDateChange"> => {
|
||||
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 });
|
||||
return { dateValue, onDateChange: setDateValue };
|
||||
};
|
||||
|
||||
export const useScopedDailyActivityRange = (
|
||||
accessToken: string | null,
|
||||
scope: DailyActivityScope,
|
||||
): DailyActivityRange => {
|
||||
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 });
|
||||
const { dateValue, onDateChange } = useActivityDateRange();
|
||||
|
||||
const startTime = dateValue.from ?? null;
|
||||
const endTime = dateValue.to ?? null;
|
||||
|
|
@ -63,7 +68,7 @@ export const useScopedDailyActivityRange = (
|
|||
|
||||
return {
|
||||
dateValue,
|
||||
onDateChange: setDateValue,
|
||||
onDateChange,
|
||||
results: data.results as DailyData[],
|
||||
loading,
|
||||
isFetchingMore,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
|
||||
import KeyAutoRouterUsageTab from "./KeyAutoRouterUsageTab";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: () => ({ accessToken: "test-token", userId: "admin-123", userRole: "Admin" }),
|
||||
}));
|
||||
|
||||
const jsonResponse = (body: unknown) =>
|
||||
new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
|
||||
|
||||
const cache = {
|
||||
coverage_pct: 100,
|
||||
hit_rate_pct: 50,
|
||||
same_model: { turns: 2, hits: 1, hit_rate_pct: 50 },
|
||||
first_visit: { turns: 1, hits: 0, hit_rate_pct: 0 },
|
||||
return_to_tier: { turns: 1, hits: 1, hit_rate_pct: 100 },
|
||||
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 stats = {
|
||||
sessions: 2,
|
||||
turns: 4,
|
||||
avg_turns_per_session: 2,
|
||||
avg_session_seconds: 30,
|
||||
avg_tokens_per_session: 100,
|
||||
spend: 1.25,
|
||||
saved_spend: 8.75,
|
||||
baseline_spend: 10,
|
||||
saved_pct: 87.5,
|
||||
saved_per_session: 4.375,
|
||||
cache,
|
||||
};
|
||||
|
||||
const benchmarks = {
|
||||
start_date: "2025-01-01",
|
||||
end_date: "2025-01-31",
|
||||
routers_in_scope: 2,
|
||||
totals: stats,
|
||||
groups: [
|
||||
{ router_name: "router-one", router_type: "complexity", tier_turns: { SIMPLE: 4 }, ...stats },
|
||||
{
|
||||
router_name: "router-two",
|
||||
router_type: "complexity",
|
||||
tier_turns: { SIMPLE: 1 },
|
||||
...stats,
|
||||
spend: 0.25,
|
||||
saved_spend: 0.75,
|
||||
baseline_spend: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const noDeployments = { data: [], total_count: 0, current_page: 1, total_pages: 1, size: 1000 };
|
||||
const fetchMock = vi.fn(async (request: Request | string) => {
|
||||
const url = typeof request === "string" ? request : request.url;
|
||||
if (url.includes("/auto_router/benchmarks")) return jsonResponse(benchmarks);
|
||||
return jsonResponse(noDeployments);
|
||||
});
|
||||
const requestedUrls = () =>
|
||||
fetchMock.mock.calls.map(([request]) => (typeof request === "string" ? request : request.url));
|
||||
|
||||
describe("KeyAutoRouterUsageTab", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testQueryClient.clear();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
});
|
||||
|
||||
it("renders this key's spend, baseline, savings and per-router filter", async () => {
|
||||
renderWithProviders(<KeyAutoRouterUsageTab accessToken="test-token" keyToken="key-hash-1" />);
|
||||
|
||||
expect(await screen.findByText("$8.75")).toBeInTheDocument();
|
||||
expect(screen.getByText("Actual auto-router spend")).toBeInTheDocument();
|
||||
expect(screen.getByText("$1.25")).toBeInTheDocument();
|
||||
expect(screen.getByText("Estimated spend at highest-tier model")).toBeInTheDocument();
|
||||
expect(screen.getByText("$10.00")).toBeInTheDocument();
|
||||
expect(screen.getByText("Auto-router prompt caching")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("50.0%").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("All auto-routers")).toBeInTheDocument();
|
||||
|
||||
const benchmarkUrl = new URL(requestedUrls().find((url) => url.includes("/auto_router/benchmarks")) ?? "");
|
||||
expect(benchmarkUrl.searchParams.get("api_key")).toBe("key-hash-1");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { AutoRouterUsageView } from "@/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab";
|
||||
import { useActivityDateRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange";
|
||||
|
||||
interface KeyAutoRouterUsageTabProps {
|
||||
accessToken: string | null;
|
||||
keyToken: string;
|
||||
}
|
||||
|
||||
const KeyAutoRouterUsageTab: React.FC<KeyAutoRouterUsageTabProps> = ({ accessToken, keyToken }) => (
|
||||
<AutoRouterUsageView accessToken={accessToken} activity={useActivityDateRange()} apiKey={keyToken} />
|
||||
);
|
||||
|
||||
export default KeyAutoRouterUsageTab;
|
||||
|
|
@ -26,6 +26,10 @@ vi.mock("./key_edit_view", () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock("./KeyAutoRouterUsageTab", () => ({
|
||||
default: ({ keyToken }: { keyToken: string }) => <div data-testid="key-auto-router-usage">{keyToken}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
|
@ -176,6 +180,22 @@ describe("KeyInfoView", () => {
|
|||
await userEvent.click(await screen.findByRole("button", { name: /more key actions/i }));
|
||||
};
|
||||
|
||||
it("shows key-scoped auto-router usage as its own admin tab", async () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue({ ...baseUseAuthorizedMock, userRole: "Admin" });
|
||||
renderWithProviders(<KeyInfoView keyData={MOCK_KEY_DATA} onClose={() => {}} keyId="test-key-id" teams={[]} />);
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Auto-router usage" }));
|
||||
|
||||
expect(screen.getByTestId("key-auto-router-usage")).toHaveTextContent("test-token-123");
|
||||
});
|
||||
|
||||
it("does not offer the admin-only auto-router usage tab to an internal user", () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue({ ...baseUseAuthorizedMock, userRole: "Internal User" });
|
||||
renderWithProviders(<KeyInfoView keyData={MOCK_KEY_DATA} onClose={() => {}} keyId="test-key-id" teams={[]} />);
|
||||
|
||||
expect(screen.queryByRole("tab", { name: "Auto-router usage" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("last updated", () => {
|
||||
const renderWithTimestamps = (overrides: Partial<KeyResponse>) => {
|
||||
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,14 @@ import { modelGroupHref, teamDetailHref } from "@/utils/entityLinks";
|
|||
import { BadgeLink } from "@/components/shared/BadgeLink";
|
||||
import { KeyInfoHeader } from "./KeyInfoHeader";
|
||||
import KeySavingsTab from "./KeySavingsTab";
|
||||
import KeyAutoRouterUsageTab from "./KeyAutoRouterUsageTab";
|
||||
import { useEffect, useState } from "react";
|
||||
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
|
||||
import {
|
||||
hasProxyWideSpendView,
|
||||
isProxyAdminRole,
|
||||
isUserTeamAdminForSingleTeam,
|
||||
rolesWithWriteAccess,
|
||||
} from "../../utils/roles";
|
||||
import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "../callback_info_helpers";
|
||||
import AutoRotationView from "../common_components/AutoRotationView";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
|
|
@ -618,6 +624,11 @@ export default function KeyInfoView({
|
|||
<TabsTrigger value="savings" className="flex-none rounded-none px-4 py-2">
|
||||
Savings
|
||||
</TabsTrigger>
|
||||
{hasProxyWideSpendView(userRole) && (
|
||||
<TabsTrigger value="auto-router-usage" className="flex-none rounded-none px-4 py-2">
|
||||
Auto-router usage
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="settings" className="flex-none rounded-none px-4 py-2">
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
|
|
@ -764,6 +775,12 @@ export default function KeyInfoView({
|
|||
/>
|
||||
</TabsContent>
|
||||
|
||||
{hasProxyWideSpendView(userRole) && (
|
||||
<TabsContent value="auto-router-usage">
|
||||
<KeyAutoRouterUsageTab accessToken={accessToken} keyToken={currentKeyData.token} />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* Settings Panel */}
|
||||
<TabsContent value="settings" keepMounted>
|
||||
<Card className="block p-6">
|
||||
|
|
|
|||
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -41392,6 +41392,8 @@ export interface operations {
|
|||
start_date?: string | null;
|
||||
/** @description YYYY-MM-DD UTC, inclusive (defaults to today) */
|
||||
end_date?: string | null;
|
||||
/** @description Filter to one virtual key token hash */
|
||||
api_key?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue