From 2855e33b4f8b3543e50cb7503865c4e5ff652c74 Mon Sep 17 00:00:00 2001 From: kimsehwan96 Date: Mon, 27 Apr 2026 17:00:58 +0900 Subject: [PATCH 1/2] [Feature] Allow Admin Viewers to Access Spend Logs Add /spend/logs, /spend/logs/ui, /spend/logs/v2, /spend/logs/session/ui, /spend/logs/ui/{request_id} to admin_viewer_routes so proxy_admin_viewer can view spend logs across the platform. --- litellm/proxy/_types.py | 5 ++ .../proxy/auth/test_route_checks.py | 48 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 92c920ca594..c932225e8d3 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -727,6 +727,11 @@ class LiteLLMRoutes(enum.Enum): "/tag/list", "/audit", "/audit/{id}", + "/spend/logs", + "/spend/logs/ui", + "/spend/logs/v2", + "/spend/logs/session/ui", + "/spend/logs/ui/{request_id}", "/global/activity", "/global/activity/model", "/global/activity/cache_hits", diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index a5d405cfc2d..a13afab278c 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -1198,6 +1198,54 @@ def test_proxy_admin_viewer_can_access_audit_logs(route): ) +@pytest.mark.parametrize( + "route", + [ + "/spend/logs", + "/spend/logs/ui", + "/spend/logs/v2", + "/spend/logs/session/ui", + "/spend/logs/ui/some-request-id", + ], +) +def test_proxy_admin_viewer_can_access_spend_logs(route): + """ + Test that proxy_admin_viewer can access /spend/logs endpoints. + + The role description states "view all spend across the platform", so the + raw spend log endpoints (which back the admin Logs UI page) must be + reachable by proxy_admin_viewer. + """ + + user_obj = LiteLLM_UserTable( + user_id="viewer_user", + user_email="viewer@example.com", + user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value, + ) + + valid_token = UserAPIKeyAuth( + user_id="viewer_user", + user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value, + ) + + request = MagicMock(spec=Request) + request.query_params = {} + + try: + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + except Exception as e: + pytest.fail( + f"proxy_admin_viewer should be able to access {route} route. Got error: {str(e)}" + ) + + class TestModelsRouteExemptFromDisableLLMEndpoints: """ Test that /models and /v1/models are exempt from DISABLE_LLM_API_ENDPOINTS. From 1c8901ff710ce147134b36ebe977ae3d8d09d594 Mon Sep 17 00:00:00 2001 From: kimsehwan96 Date: Mon, 27 Apr 2026 17:20:29 +0900 Subject: [PATCH 2/2] Add /spend/logs/v2 and /spend/logs/ui/{request_id} to spend_tracking_routes These routes share their handler with /spend/logs/ui (already in spend_tracking_routes), so they must be reachable by INTERNAL_USER and INTERNAL_USER_VIEW_ONLY for parity. Per-user data filtering is still enforced inside the endpoint by _can_user_view_spend_log. --- litellm/proxy/_types.py | 2 + .../proxy/auth/test_route_checks.py | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c932225e8d3..b08ee95f450 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -586,7 +586,9 @@ class LiteLLMRoutes(enum.Enum): "/spend/calculate", "/spend/logs", "/spend/logs/ui", + "/spend/logs/v2", "/spend/logs/session/ui", + "/spend/logs/ui/{request_id}", "/cost/estimate", ] diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index a13afab278c..cb518818d22 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -1246,6 +1246,57 @@ def test_proxy_admin_viewer_can_access_spend_logs(route): ) +@pytest.mark.parametrize( + "user_role", + [ + LitellmUserRoles.INTERNAL_USER.value, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value, + ], +) +@pytest.mark.parametrize( + "route", + ["/spend/logs/v2", "/spend/logs/ui/some-request-id"], +) +def test_internal_user_can_access_v2_spend_logs(route, user_role): + """ + Test that INTERNAL_USER and INTERNAL_USER_VIEW_ONLY can pass the route + check for /spend/logs/v2 and /spend/logs/ui/{request_id}. + + These routes share their handler with /spend/logs/ui (already in + spend_tracking_routes), so they must be reachable by the same roles for + parity. Per-user data filtering is enforced separately by + _can_user_view_spend_log inside the endpoint. + """ + + user_obj = LiteLLM_UserTable( + user_id="internal_user", + user_email="internal@example.com", + user_role=user_role, + ) + + valid_token = UserAPIKeyAuth( + user_id="internal_user", + user_role=user_role, + ) + + request = MagicMock(spec=Request) + request.query_params = {} + + try: + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=user_role, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + except Exception as e: + pytest.fail( + f"{user_role} should be able to access {route} route. Got error: {str(e)}" + ) + + class TestModelsRouteExemptFromDisableLLMEndpoints: """ Test that /models and /v1/models are exempt from DISABLE_LLM_API_ENDPOINTS.