diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 07e2ca71950..711d70b9524 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -172,6 +172,11 @@ class UISettings(BaseModel): description="If true, org admins cannot generate API keys via /key/generate.", ) + disable_ui_nudges: bool = Field( + default=False, + description="If true, suppresses in-product UI nudges (survey and Claude Code feedback popups) for all users.", + ) + class UISettingsResponse(SettingsResponse): """Response model for UI settings""" @@ -195,6 +200,7 @@ ALLOWED_UI_SETTINGS_FIELDS = { "scope_user_search_to_org", "disable_custom_api_keys", "disable_key_generate_for_org_admin", + "disable_ui_nudges", } # Flags that must be synced from the persisted UISettings into diff --git a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py index ae217aca16e..f77af2d90bf 100644 --- a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py +++ b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py @@ -1032,6 +1032,45 @@ class TestProxySettingEndpoints: stored_settings = json.loads(create_data["ui_settings"]) assert stored_settings["disable_model_add_for_internal_users"] is True + def test_update_ui_settings_persists_disable_ui_nudges( + self, mock_auth, monkeypatch + ): + """disable_ui_nudges must be allowlisted so admins can suppress UI popups for everyone""" + from unittest.mock import AsyncMock, MagicMock + + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + + mock_user_auth = UserAPIKeyAuth( + user_id="test-user-123", + user_role=LitellmUserRoles.PROXY_ADMIN, + ) + app.dependency_overrides[user_api_key_auth] = lambda: mock_user_auth + + monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True) + mock_prisma = MagicMock() + mock_prisma.db.litellm_uisettings.upsert = AsyncMock() + mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma) + + try: + response = client.patch( + "/update/ui_settings", json={"disable_ui_nudges": True} + ) + finally: + app.dependency_overrides.clear() + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["settings"]["disable_ui_nudges"] is True + + create_data = mock_prisma.db.litellm_uisettings.upsert.call_args.kwargs["data"][ + "create" + ] + stored_settings = json.loads(create_data["ui_settings"]) + assert stored_settings["disable_ui_nudges"] is True + def test_update_ui_settings_ignores_non_allowlisted_value( self, mock_auth, monkeypatch ): diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index da6a0d5a76f..07398559173 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -10,6 +10,7 @@ import BudgetPanel from "@/components/budgets/budget_panel"; import CacheDashboard from "@/components/cache_dashboard"; import ClaudeCodePluginsPanel from "@/components/claude_code_plugins"; import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import LoadingScreen from "@/components/common_components/LoadingScreen"; import { CostTrackingSettings } from "@/components/CostTrackingSettings"; import GeneralSettings from "@/components/general_settings"; @@ -90,6 +91,9 @@ function CreateKeyPageContent() { const [modelData, setModelData] = useState({ data: [] }); const [createClicked, setCreateClicked] = useState(false); + const { data: uiSettingsData } = useUISettings(); + const nudgesDisabled = Boolean(uiSettingsData?.values?.disable_ui_nudges); + // Survey state - always show by default const [showSurveyPrompt, setShowSurveyPrompt] = useState(true); const [showSurveyModal, setShowSurveyModal] = useState(false); @@ -265,6 +269,9 @@ function CreateKeyPageContent() { // Fetch in-product nudges configuration from backend useEffect(() => { + if (nudgesDisabled) { + return; + } if (accessToken && token) { (async () => { try { @@ -284,7 +291,7 @@ function CreateKeyPageContent() { } })(); } - }, [accessToken, token]); + }, [accessToken, token, nudgesDisabled]); // Auto-dismiss survey prompt after 15 seconds useEffect(() => { @@ -550,7 +557,7 @@ function CreateKeyPageContent() { {/* Survey Components */} @@ -562,7 +569,7 @@ function CreateKeyPageContent() { {/* Claude Code Components */} diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index 9ce0d908838..25865c48f9b 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -26,6 +26,7 @@ export default function UISettings() { const allowVectorStoresTeamAdminsProperty = schema?.properties?.allow_vector_stores_for_team_admins; const scopeUserSearchProperty = schema?.properties?.scope_user_search_to_org; const disableCustomApiKeysProperty = schema?.properties?.disable_custom_api_keys; + const disableUINudgesProperty = schema?.properties?.disable_ui_nudges; const values = data?.values ?? {}; const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users); const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user); @@ -60,6 +61,20 @@ export default function UISettings() { ); }; + const handleToggleDisableUINudges = (checked: boolean) => { + updateSettings( + { disable_ui_nudges: checked }, + { + onSuccess: () => { + NotificationManager.success("UI settings updated successfully"); + }, + onError: (error) => { + NotificationManager.fromBackend(error); + }, + }, + ); + }; + const handleUpdatePageVisibility = (settings: { enabled_ui_pages_internal_users: string[] | null }) => { updateSettings(settings, { onSuccess: () => { @@ -451,6 +466,26 @@ export default function UISettings() { + {/* Disable in-product UI nudges */} + + + + Disable UI nudges + + {disableUINudgesProperty?.description ?? + "If true, suppresses in-product UI nudges (survey and Claude Code feedback popups) for all users."} + + + + + + {/* Page Visibility for Internal Users */}