diff --git a/enterprise/litellm_enterprise/proxy/enterprise_routes.py b/enterprise/litellm_enterprise/proxy/enterprise_routes.py index e28d8b8a4c..ec37c04980 100644 --- a/enterprise/litellm_enterprise/proxy/enterprise_routes.py +++ b/enterprise/litellm_enterprise/proxy/enterprise_routes.py @@ -4,10 +4,13 @@ from litellm_enterprise.enterprise_callbacks.send_emails.endpoints import ( router as email_events_router, ) +from . import ui_crud_endpoints # side-effect: registers extra UI settings from .audit_logging_endpoints import router as audit_logging_router from .management_endpoints import management_endpoints_router from .utils import _should_block_robots +__all__ = ["router", "ui_crud_endpoints"] + router = APIRouter() router.include_router(email_events_router) router.include_router(audit_logging_router) diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/__init__.py b/enterprise/litellm_enterprise/proxy/management_endpoints/__init__.py index 7042dae53a..0791a061b4 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/__init__.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/__init__.py @@ -1,8 +1,10 @@ from fastapi import APIRouter from .internal_user_endpoints import router as internal_user_endpoints_router +from .project_endpoints import router as project_endpoints_router management_endpoints_router = APIRouter() management_endpoints_router.include_router(internal_user_endpoints_router) +management_endpoints_router.include_router(project_endpoints_router) __all__ = ["management_endpoints_router"] diff --git a/litellm/proxy/management_endpoints/project_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py similarity index 100% rename from litellm/proxy/management_endpoints/project_endpoints.py rename to enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py diff --git a/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py b/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py new file mode 100644 index 0000000000..296d964f85 --- /dev/null +++ b/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py @@ -0,0 +1,3 @@ +from . import ui_settings_extensions # side-effect: registers extra UI settings fields + +__all__ = ["ui_settings_extensions"] diff --git a/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py b/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py new file mode 100644 index 0000000000..20289cecac --- /dev/null +++ b/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py @@ -0,0 +1,24 @@ +"""Enterprise-only UI settings fields. + +Registers additional fields onto the OSS ``UISettings`` model at import time. +Importing this module has the side effect of extending both the GET schema +and the PATCH allowlist served by ``/get/ui_settings`` and +``/update/ui_settings``. +""" +from pydantic import Field + +from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( + register_extra_ui_setting, +) + +register_extra_ui_setting( + "enable_projects_ui", + bool, + Field( + default=False, + description=( + "If enabled, shows the Projects feature in the UI sidebar and " + "the project field in key management." + ), + ), +) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 85a12f70f5..df003f1894 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -407,9 +407,6 @@ from litellm.proxy.management_endpoints.organization_endpoints import ( router as organization_router, ) from litellm.proxy.management_endpoints.policy_endpoints import router as policy_router -from litellm.proxy.management_endpoints.project_endpoints import ( - router as project_router, -) from litellm.proxy.management_endpoints.router_settings_endpoints import ( router as router_settings_router, ) @@ -13911,7 +13908,6 @@ app.include_router(team_router) app.include_router(ui_sso_router) app.include_router(scim_router) app.include_router(organization_router) -app.include_router(project_router) app.include_router(customer_router) app.include_router(spend_management_router) app.include_router(cloudzero_router) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 0349f289b4..5cf7c215cf 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -4,6 +4,8 @@ from typing import Any, Dict, List, Optional, Union from urllib.parse import urlparse from fastapi import APIRouter, Depends, File, HTTPException, UploadFile +from pydantic import ConfigDict, create_model +from pydantic.fields import FieldInfo import litellm from litellm._logging import verbose_proxy_logger @@ -75,6 +77,8 @@ class UIThemeSettingsResponse(SettingsResponse): class UISettings(BaseModel): """Configuration for UI-specific flags""" + model_config = ConfigDict(extra="allow") + disable_model_add_for_internal_users: bool = Field( default=False, description="If true, internal users cannot add models from the UI", @@ -100,11 +104,6 @@ class UISettings(BaseModel): description="If enabled, forwards client headers (e.g. Authorization) to the LLM API. Required for Claude Code with Max subscription.", ) - enable_projects_ui: bool = Field( - default=False, - description="If enabled, shows the Projects feature in the UI sidebar and the project field in key management.", - ) - disable_agents_for_internal_users: bool = Field( default=False, description="If true, internal users cannot access agent management endpoints or the Agents page in the UI.", @@ -149,7 +148,6 @@ ALLOWED_UI_SETTINGS_FIELDS = { "enabled_ui_pages_internal_users", "require_auth_for_public_ai_hub", "forward_client_headers_to_llm_api", - "enable_projects_ui", "disable_agents_for_internal_users", "allow_agents_for_team_admins", "disable_vector_stores_for_internal_users", @@ -168,6 +166,36 @@ _RUNTIME_GENERAL_SETTINGS_FLAGS = [ "allow_vector_stores_for_team_admins", ] +# Extension point: packages outside OSS (e.g. litellm_enterprise) can +# contribute additional UI settings fields at import time. Each entry +# maps a field name to a (type, FieldInfo) tuple suitable for pydantic's +# create_model. Registering a field also appends it to +# ALLOWED_UI_SETTINGS_FIELDS so GET/PATCH pass it through. +_EXTRA_UI_SETTINGS_FIELDS: Dict[str, tuple] = {} + +# Settings OSS knows about as enterprise-gated. If a caller sends one of +# these keys and no extension package has registered it, the PATCH +# endpoint returns 403 instead of silently dropping the value, so the +# client gets a clear signal that the feature requires LiteLLM Enterprise. +_ENTERPRISE_ONLY_UI_SETTINGS: set[str] = {"enable_projects_ui"} + + +def register_extra_ui_setting(name: str, type_: Any, field: FieldInfo) -> None: + """Register an additional UI settings field contributed by an extension package.""" + _EXTRA_UI_SETTINGS_FIELDS[name] = (type_, field) + ALLOWED_UI_SETTINGS_FIELDS.add(name) + + +def _get_effective_ui_settings_class() -> type: + """Return UISettings with any extension-registered fields merged in.""" + if not _EXTRA_UI_SETTINGS_FIELDS: + return UISettings + return create_model( + "EffectiveUISettings", + __base__=UISettings, + **_EXTRA_UI_SETTINGS_FIELDS, + ) + class MCPSemanticFilterSettings(BaseModel): """Configuration for MCP Semantic Tool Filter""" @@ -1136,7 +1164,7 @@ async def get_ui_settings(): return await _get_settings_with_schema( settings_key="ui_settings", - settings_class=UISettings, + settings_class=_get_effective_ui_settings_class(), config=config, ) @@ -1177,6 +1205,23 @@ async def update_ui_settings( # Only include fields the caller actually sent (not Pydantic defaults). settings_dict = settings.model_dump(exclude_unset=True) + # Reject enterprise-only settings up front so the caller gets a clear + # signal instead of a silent drop. + blocked_enterprise_keys = sorted( + (settings_dict.keys() & _ENTERPRISE_ONLY_UI_SETTINGS) + - ALLOWED_UI_SETTINGS_FIELDS + ) + if blocked_enterprise_keys: + raise HTTPException( + status_code=403, + detail={ + "error": ( + f"Setting(s) {blocked_enterprise_keys} are a LiteLLM " + "Enterprise feature and are not available on this build." + ) + }, + ) + # Enforce allowlist and drop anything unexpected incoming = { k: v for k, v in settings_dict.items() if k in ALLOWED_UI_SETTINGS_FIELDS diff --git a/tests/proxy_unit_tests/test_project_endpoints_prisma.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py similarity index 99% rename from tests/proxy_unit_tests/test_project_endpoints_prisma.py rename to tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index 77ed09a40f..8d72885e76 100644 --- a/tests/proxy_unit_tests/test_project_endpoints_prisma.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -20,7 +20,7 @@ from litellm._logging import verbose_proxy_logger from litellm.proxy.management_endpoints.team_endpoints import ( new_team, ) -from litellm.proxy.management_endpoints.project_endpoints import ( +from litellm_enterprise.proxy.management_endpoints.project_endpoints import ( new_project, update_project, delete_project, 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 fb7c38449b..e1d3c1609b 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -284,22 +284,24 @@ export default function UISettings() { - - - - [BETA] Enable Projects (page will refresh) - - {enableProjectsUIProperty?.description ?? - "If enabled, shows the Projects feature in the UI sidebar and the project field in key management."} - + {enableProjectsUIProperty && ( + + + + [BETA] Enable Projects (page will refresh) + + {enableProjectsUIProperty.description ?? + "If enabled, shows the Projects feature in the UI sidebar and the project field in key management."} + + - + )}