From b36d252b265cc3dc2b8d41d69f9274f65e50713c Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Sat, 5 Sep 2026 18:38:25 -0700 Subject: [PATCH] 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 --- litellm/proxy/db/autorouter_session_rollup.py | 4 +- .../auto_router_endpoints.py | 2 + .../spend/test_autorouter_session_rollup.py | 32 +++++++ .../test_auto_router_endpoints.py | 5 +- .../AutoRouterBenchmarksTab.test.tsx | 28 +++++- .../_components/AutoRouterBenchmarksTab.tsx | 9 +- .../useAutoRouterBenchmarks.test.ts | 31 ++++++- .../_components/useAutoRouterBenchmarks.ts | 4 +- .../useDailyActivityRange.test.tsx | 10 +- .../_components/useDailyActivityRange.ts | 13 ++- ...KeyAutoRouterUsageTab.integration.test.tsx | 92 +++++++++++++++++++ .../templates/KeyAutoRouterUsageTab.tsx | 17 ++++ .../templates/key_info_view.test.tsx | 20 ++++ .../components/templates/key_info_view.tsx | 19 +++- ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 + 15 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx create mode 100644 ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx diff --git a/litellm/proxy/db/autorouter_session_rollup.py b/litellm/proxy/db/autorouter_session_rollup.py index 9c637a62dc1..b33bffbaaa1 100644 --- a/litellm/proxy/db/autorouter_session_rollup.py +++ b/litellm/proxy/db/autorouter_session_rollup.py @@ -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 diff --git a/litellm/proxy/management_endpoints/auto_router_endpoints.py b/litellm/proxy/management_endpoints/auto_router_endpoints.py index 2a7813bc140..0f0323b45f8 100644 --- a/litellm/proxy/management_endpoints/auto_router_endpoints.py +++ b/litellm/proxy/management_endpoints/auto_router_endpoints.py @@ -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 = ( diff --git a/tests/proxy_behavior/spend/test_autorouter_session_rollup.py b/tests/proxy_behavior/spend/test_autorouter_session_rollup.py index 7bc61c40ea0..c2272f3d20d 100644 --- a/tests/proxy_behavior/spend/test_autorouter_session_rollup.py +++ b/tests/proxy_behavior/spend/test_autorouter_session_rollup.py @@ -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"] == {} diff --git a/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py index 21f8d985f22..35e76c96c14 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py @@ -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 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx index ce0cd75cd36..e7c6adc478c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx @@ -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( + + + , + ); + + 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(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx index 09a0cf0242b..39e6b0fd390 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx @@ -273,12 +273,13 @@ const BenchmarksBody: React.FC = ({ isPending, error, data, interface AutoRouterBenchmarksTabProps { accessToken: string | null; - activity: DailyActivityRange; + activity: Pick; + apiKey?: string; } -const UsageView: React.FC = ({ accessToken, activity }) => { +export const AutoRouterUsageView: React.FC = ({ 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(ALL_ROUTERS); const { data: autoRouters } = useAutoRouters(); @@ -347,7 +348,7 @@ const AutoRouterBenchmarksTab: React.FC = ({ acces - + diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.test.ts index e8a858ef3a5..1258c967ec9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.test.ts @@ -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 } }, + ]; + 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 } }, + ]; + expect(init.params.query.api_key).toBeUndefined(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts index 155c9d0c696..284342f0aec 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts @@ -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 }, ); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx index 6c9281060a4..00902aa9fdd 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx @@ -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")); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts index 8ed57e36a94..234144f4423 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts @@ -37,13 +37,18 @@ export interface DailyActivityScope { apiKey?: string | null; } +export const useActivityDateRange = (): Pick => { + const initialFrom = useMemo(() => new Date(new Date().getTime() - THIRTY_DAYS_MS), []); + const initialTo = useMemo(() => new Date(), []); + const [dateValue, setDateValue] = useState({ 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({ 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, diff --git a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx new file mode 100644 index 00000000000..69d2f1b896b --- /dev/null +++ b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx @@ -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(); + + 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"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx new file mode 100644 index 00000000000..a2a7b9079a5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx @@ -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 = ({ accessToken, keyToken }) => ( + +); + +export default KeyAutoRouterUsageTab; 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 7817d4e7cea..feee1cee9e6 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 @@ -26,6 +26,10 @@ vi.mock("./key_edit_view", () => ({ }, })); +vi.mock("./KeyAutoRouterUsageTab", () => ({ + default: ({ keyToken }: { keyToken: string }) =>
{keyToken}
, +})); + 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( {}} 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( {}} keyId="test-key-id" teams={[]} />); + + expect(screen.queryByRole("tab", { name: "Auto-router usage" })).not.toBeInTheDocument(); + }); + describe("last updated", () => { const renderWithTimestamps = (overrides: Partial) => { vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock); 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 0dd0dd6d6af..86bbfd73410 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -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({ Savings + {hasProxyWideSpendView(userRole) && ( + + Auto-router usage + + )} Settings @@ -764,6 +775,12 @@ export default function KeyInfoView({ />
+ {hasProxyWideSpendView(userRole) && ( + + + + )} + {/* Settings Panel */} diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index b1534c19670..799f0e7ef32 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -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;