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 (