diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 76a51627d0c..66a00e14f15 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -633,6 +633,7 @@ class LiteLLMRoutes(enum.Enum): "/v1/models", "/sso/get/ui_settings", "/get/user_banner", + "/get/latest_release_info", ] # NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index 72c59223549..df5ac4ac135 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -120,6 +120,33 @@ def test_user_banner_read_open_to_non_admin_roles(role): ) +@pytest.mark.parametrize( + "role", + [ + LitellmUserRoles.INTERNAL_USER.value, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value, + ], +) +def test_latest_release_info_read_open_to_non_admin_roles(role): + user_obj = LiteLLM_UserTable( + user_id="test_user", + user_email="test@example.com", + user_role=role, + ) + valid_token = UserAPIKeyAuth(user_id="test_user", user_role=role) + request = MagicMock(spec=Request) + request.query_params = {} + + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=role, + route="/get/latest_release_info", + request=request, + valid_token=valid_token, + request_data={}, + ) + + def test_user_banner_update_rejected_for_non_admin(): """Publishing the banner stays admin-only at the route layer.""" user_obj = LiteLLM_UserTable( diff --git a/ui/litellm-dashboard/src/components/UpgradeBanner.test.tsx b/ui/litellm-dashboard/src/components/UpgradeBanner.test.tsx index 831030fee80..4466597394c 100644 --- a/ui/litellm-dashboard/src/components/UpgradeBanner.test.tsx +++ b/ui/litellm-dashboard/src/components/UpgradeBanner.test.tsx @@ -82,6 +82,14 @@ describe("UpgradeBannerView", () => { render(); expect(screen.getByRole("status")).toHaveTextContent("The latest version is v1.104.0"); }); + + it("shows a newer release after the current one was dismissed without remounting", () => { + const { rerender } = render(); + fireEvent.click(screen.getByRole("button", { name: "Close" })); + expect(screen.queryByRole("status")).not.toBeInTheDocument(); + rerender(); + expect(screen.getByRole("status")).toHaveTextContent("The latest version is v1.104.0"); + }); }); describe("UpgradeBanner", () => { diff --git a/ui/litellm-dashboard/src/components/UpgradeBanner.tsx b/ui/litellm-dashboard/src/components/UpgradeBanner.tsx index d990c5112a4..101a6e3410d 100644 --- a/ui/litellm-dashboard/src/components/UpgradeBanner.tsx +++ b/ui/litellm-dashboard/src/components/UpgradeBanner.tsx @@ -34,20 +34,20 @@ export const describeRelease = ({ new_features, bug_fixes, other_updates }: Late ].join(", "); export const UpgradeBannerView: React.FC = ({ currentVersion, latestRelease }) => { - const [locallyDismissed, setLocallyDismissed] = useState(false); + const [dismissedVersion, setDismissedVersion] = useState(null); if (!currentVersion || !latestRelease || !isNewerVersion(currentVersion, latestRelease.version)) { return null; } const dismissKey = `${DISMISS_KEY_PREFIX}${latestRelease.version}`; - if (locallyDismissed || getLocalStorageItem(dismissKey) === "true") { + if (dismissedVersion === latestRelease.version || getLocalStorageItem(dismissKey) === "true") { return null; } const handleClose = () => { setLocalStorageItem(dismissKey, "true"); - setLocallyDismissed(true); + setDismissedVersion(latestRelease.version); }; return (