[Refactor] Proxy: move projects management to enterprise package

Remove the /project/* management endpoints and the enable_projects_ui
admin-settings flag from the OSS litellm package. Project endpoints now
live under litellm_enterprise and are wired through the existing
enterprise router; OSS builds return 404 for every /project/* route.

The enable_projects_ui UI flag is registered back onto UISettings via a
small extension registry when the enterprise package is imported, so the
admin toggle and downstream key/sidebar gating continue to work in
enterprise builds. On OSS, explicit PATCH attempts with the flag return
403 with a clear enterprise-only message instead of being silently
dropped.

Pydantic request/response types (NewProjectRequest, UpdateProjectRequest,
DeleteProjectRequest, NewProjectResponse) stay in litellm/proxy/_types.py
because management_endpoints/common_utils.py and pydantic-shape tests
import them. LiteLLM_ProjectTable and all FK columns in schema.prisma
are unchanged.
This commit is contained in:
Yuneng Jiang 2026-04-13 21:41:12 -07:00
parent e64d98f725
commit d3a1f63af2
No known key found for this signature in database
9 changed files with 102 additions and 27 deletions

View File

@ -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)

View File

@ -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"]

View File

@ -0,0 +1,3 @@
from . import ui_settings_extensions # side-effect: registers extra UI settings fields
__all__ = ["ui_settings_extensions"]

View File

@ -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."
),
),
)

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -284,22 +284,24 @@ export default function UISettings() {
</Space>
</Space>
<Space align="start" size="middle">
<Switch
checked={Boolean(values.enable_projects_ui)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleEnableProjectsUI}
aria-label={enableProjectsUIProperty?.description ?? "Enable Projects UI"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>[BETA] Enable Projects (page will refresh)</Typography.Text>
<Typography.Text type="secondary">
{enableProjectsUIProperty?.description ??
"If enabled, shows the Projects feature in the UI sidebar and the project field in key management."}
</Typography.Text>
{enableProjectsUIProperty && (
<Space align="start" size="middle">
<Switch
checked={Boolean(values.enable_projects_ui)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleEnableProjectsUI}
aria-label={enableProjectsUIProperty.description ?? "Enable Projects UI"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>[BETA] Enable Projects (page will refresh)</Typography.Text>
<Typography.Text type="secondary">
{enableProjectsUIProperty.description ??
"If enabled, shows the Projects feature in the UI sidebar and the project field in key management."}
</Typography.Text>
</Space>
</Space>
</Space>
)}
<Divider />