diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index a7efe97157..db3ae9ad94 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -94,6 +94,11 @@ class UISettings(BaseModel): description="List of page keys that internal users (non-admins) can see in the UI sidebar. If not set, all pages are visible based on role permissions.", ) + require_auth_for_public_ai_hub: bool = Field( + default=False, + description="If true, requires authentication for accessing the public AI Hub.", + ) + allow_public_health_readiness_details: bool = Field( default=False, description="If true, returns the legacy detailed payload from the unauthenticated /health/readiness endpoint.", @@ -168,6 +173,7 @@ ALLOWED_UI_SETTINGS_FIELDS = { "disable_model_add_for_internal_users", "disable_team_admin_delete_team_user", "enabled_ui_pages_internal_users", + "require_auth_for_public_ai_hub", "allow_public_health_readiness_details", "forward_client_headers_to_llm_api", "forward_llm_provider_auth_headers", 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 8e67c7e87e..c27d7eedcd 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 @@ -868,6 +868,7 @@ class TestProxySettingEndpoints: mock_db_record = MagicMock() mock_db_record.ui_settings = { "disable_model_add_for_internal_users": True, + "require_auth_for_public_ai_hub": True, "unexpected_flag": True, } mock_prisma.db.litellm_uisettings.find_unique = AsyncMock( @@ -880,10 +881,12 @@ class TestProxySettingEndpoints: assert response.status_code == 200 data = response.json() assert data["values"]["disable_model_add_for_internal_users"] is True + assert data["values"]["require_auth_for_public_ai_hub"] is True assert "unexpected_flag" not in data["values"] assert ( "disable_model_add_for_internal_users" in data["field_schema"]["properties"] ) + assert "require_auth_for_public_ai_hub" in data["field_schema"]["properties"] mock_prisma.db.litellm_uisettings.find_unique.assert_called_once_with( where={"id": "ui_settings"} ) @@ -1070,6 +1073,43 @@ class TestProxySettingEndpoints: assert "unsupported_flag" not in stored_settings assert stored_settings["disable_model_add_for_internal_users"] is False + def test_update_ui_settings_preserves_public_ai_hub_auth_flag( + self, mock_auth, monkeypatch + ): + """Public AI Hub auth is an existing UI setting and must remain writable.""" + 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) + + payload = {"require_auth_for_public_ai_hub": True} + + try: + response = client.patch("/update/ui_settings", json=payload) + finally: + app.dependency_overrides.clear() + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["settings"]["require_auth_for_public_ai_hub"] is True + + call_args = mock_prisma.db.litellm_uisettings.upsert.call_args + stored_settings = json.loads(call_args.kwargs["data"]["create"]["ui_settings"]) + assert stored_settings["require_auth_for_public_ai_hub"] is True + def test_update_ui_settings_persists_forward_llm_provider_auth_headers( self, mock_auth, monkeypatch ):