Merge pull request #25677 from BerriAI/litellm_migration_projects
[Refactor] Proxy: move projects management to enterprise package
This commit is contained in:
commit
7723a54478
1
.github/workflows/test-unit-proxy-db.yml
vendored
1
.github/workflows/test-unit-proxy-db.yml
vendored
@ -183,7 +183,6 @@ jobs:
|
||||
tests/proxy_unit_tests/test_skills_db.py
|
||||
tests/proxy_unit_tests/test_update_daily_tag_spend.py
|
||||
tests/proxy_unit_tests/test_update_spend.py
|
||||
tests/proxy_unit_tests/test_project_endpoints_prisma.py
|
||||
tests/proxy_unit_tests/test_proxy_encrypt_decrypt.py
|
||||
workers: 4
|
||||
dist: loadscope
|
||||
|
||||
2
.github/workflows/test-unit-proxy-legacy.yml
vendored
2
.github/workflows/test-unit-proxy-legacy.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
- name: "key-generation"
|
||||
path: "tests/proxy_unit_tests/test_[k-o]*.py"
|
||||
- name: "proxy-config"
|
||||
path: "tests/proxy_unit_tests/test_prisma*.py tests/proxy_unit_tests/test_project*.py tests/proxy_unit_tests/test_prompt*.py tests/proxy_unit_tests/test_proxy_[c-r]*.py"
|
||||
path: "tests/proxy_unit_tests/test_prisma*.py tests/proxy_unit_tests/test_prompt*.py tests/proxy_unit_tests/test_proxy_[c-r]*.py"
|
||||
- name: "proxy-server"
|
||||
path: "tests/proxy_unit_tests/test_proxy_server.py"
|
||||
- name: "proxy-server-extras"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
from . import ui_settings_extensions # side-effect: registers extra UI settings fields
|
||||
|
||||
__all__ = ["ui_settings_extensions"]
|
||||
@ -0,0 +1,25 @@
|
||||
"""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.fields import FieldInfo
|
||||
|
||||
from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import (
|
||||
register_extra_ui_setting,
|
||||
)
|
||||
|
||||
register_extra_ui_setting(
|
||||
"enable_projects_ui",
|
||||
bool,
|
||||
FieldInfo(
|
||||
default=False,
|
||||
description=(
|
||||
"If enabled, shows the Projects feature in the UI sidebar and "
|
||||
"the project field in key management."
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -14195,7 +14192,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)
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
#### CRUD ENDPOINTS for UI Settings #####
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
|
||||
from pydantic import ConfigDict, ValidationError, 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",
|
||||
@ -117,11 +121,6 @@ class UISettings(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
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.",
|
||||
@ -167,7 +166,6 @@ ALLOWED_UI_SETTINGS_FIELDS = {
|
||||
"require_auth_for_public_ai_hub",
|
||||
"forward_client_headers_to_llm_api",
|
||||
"forward_llm_provider_auth_headers",
|
||||
"enable_projects_ui",
|
||||
"disable_agents_for_internal_users",
|
||||
"allow_agents_for_team_admins",
|
||||
"disable_vector_stores_for_internal_users",
|
||||
@ -187,6 +185,61 @@ _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 (annotation, FieldInfo) tuple in pydantic
|
||||
# create_model's field-definitions format. Registering a field also
|
||||
# appends it to ALLOWED_UI_SETTINGS_FIELDS so GET/PATCH pass it through.
|
||||
#
|
||||
# The annotation is typed ``Any`` because pydantic field annotations
|
||||
# include generics like ``Optional[int]`` / ``List[str]`` that are not
|
||||
# instances of ``type`` — so tightening this to ``type`` would reject
|
||||
# valid inputs.
|
||||
_EXTRA_UI_SETTINGS_FIELDS: Dict[str, Tuple[Any, FieldInfo]] = {}
|
||||
|
||||
# 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"}
|
||||
|
||||
# Memoized effective class; invalidated on registration.
|
||||
_EFFECTIVE_UI_SETTINGS_CLASS: Optional[Type[UISettings]] = None
|
||||
|
||||
|
||||
def register_extra_ui_setting(name: str, annotation: Any, field: FieldInfo) -> None:
|
||||
"""Register an additional UI settings field contributed by an extension package.
|
||||
|
||||
``field`` must be a ``FieldInfo`` instance — construct it directly
|
||||
(e.g. ``FieldInfo(default=..., description=...)``) rather than via
|
||||
the ``pydantic.Field`` factory, whose stub reports the default's
|
||||
type instead of ``FieldInfo`` and trips mypy at the call site.
|
||||
"""
|
||||
global _EFFECTIVE_UI_SETTINGS_CLASS
|
||||
_EXTRA_UI_SETTINGS_FIELDS[name] = (annotation, field)
|
||||
ALLOWED_UI_SETTINGS_FIELDS.add(name)
|
||||
_EFFECTIVE_UI_SETTINGS_CLASS = None
|
||||
|
||||
|
||||
def _get_effective_ui_settings_class() -> Type[UISettings]:
|
||||
"""Return UISettings with any extension-registered fields merged in.
|
||||
|
||||
Memoized — pydantic ``create_model`` runs metaclass + schema work
|
||||
each call, so we cache until a new registration invalidates it.
|
||||
"""
|
||||
global _EFFECTIVE_UI_SETTINGS_CLASS
|
||||
if _EFFECTIVE_UI_SETTINGS_CLASS is not None:
|
||||
return _EFFECTIVE_UI_SETTINGS_CLASS
|
||||
if not _EXTRA_UI_SETTINGS_FIELDS:
|
||||
return UISettings
|
||||
_EFFECTIVE_UI_SETTINGS_CLASS = create_model( # type: ignore[call-overload]
|
||||
"EffectiveUISettings",
|
||||
__base__=UISettings,
|
||||
__doc__=UISettings.__doc__,
|
||||
**_EXTRA_UI_SETTINGS_FIELDS,
|
||||
)
|
||||
return _EFFECTIVE_UI_SETTINGS_CLASS
|
||||
|
||||
|
||||
class MCPSemanticFilterSettings(BaseModel):
|
||||
"""Configuration for MCP Semantic Tool Filter"""
|
||||
@ -1155,7 +1208,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,
|
||||
)
|
||||
|
||||
@ -1166,7 +1219,8 @@ async def get_ui_settings():
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def update_ui_settings(
|
||||
settings: UISettings, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth)
|
||||
settings_body: Dict[str, Any] = Body(...),
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update UI-specific configuration flags.
|
||||
@ -1193,9 +1247,34 @@ async def update_ui_settings(
|
||||
},
|
||||
)
|
||||
|
||||
# Validate against the same effective class GET advertises, so
|
||||
# enterprise-registered fields are typed consistently on both sides.
|
||||
effective_cls = _get_effective_ui_settings_class()
|
||||
try:
|
||||
settings = effective_cls.model_validate(settings_body)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=e.errors())
|
||||
|
||||
# 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
|
||||
|
||||
@ -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,
|
||||
@ -432,7 +432,7 @@ def test_check_team_project_limits_models_not_in_team():
|
||||
"""
|
||||
Test that creating a project with models not in the team raises an error.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -458,7 +458,7 @@ def test_check_team_project_limits_budget_exceeds_team():
|
||||
"""
|
||||
Test that creating a project with budget > team budget raises an error.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -485,7 +485,7 @@ def test_check_team_project_limits_valid_subset():
|
||||
"""
|
||||
Test that a valid project (models subset, budget within limit) passes.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -510,7 +510,7 @@ def test_check_team_project_limits_all_proxy_models():
|
||||
"""
|
||||
Test that team with 'all-proxy-models' allows any project models.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -533,7 +533,7 @@ def test_check_team_project_limits_tpm_exceeds_team():
|
||||
"""
|
||||
Test that project tpm_limit exceeding team tpm_limit raises an error.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -560,7 +560,7 @@ def test_check_team_project_limits_negative_budget():
|
||||
"""
|
||||
Test that negative budget values raise an error.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -586,7 +586,7 @@ def test_check_team_project_limits_soft_budget_gte_max():
|
||||
"""
|
||||
Test that soft_budget >= max_budget raises an error.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import (
|
||||
_check_team_project_limits,
|
||||
)
|
||||
from litellm.proxy._types import LiteLLM_TeamTable
|
||||
@ -801,7 +801,7 @@ async def test_list_projects_returns_timestamps():
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from litellm.proxy.management_endpoints.project_endpoints import list_projects
|
||||
from litellm_enterprise.proxy.management_endpoints.project_endpoints import list_projects
|
||||
from litellm.proxy._types import LiteLLM_ProjectTable
|
||||
|
||||
now = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
@ -888,6 +888,55 @@ class TestProxySettingEndpoints:
|
||||
where={"id": "ui_settings"}
|
||||
)
|
||||
|
||||
def test_get_ui_settings_schema_description_preserved_with_extensions(
|
||||
self, mock_auth, monkeypatch
|
||||
):
|
||||
"""The UI renders ``schema.description`` as a header paragraph.
|
||||
When an extension package registers extra fields, the effective
|
||||
class is built via ``create_model`` — which drops the base
|
||||
class docstring unless we pass ``__doc__`` explicitly."""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from litellm.proxy.ui_crud_endpoints import proxy_setting_endpoints
|
||||
from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import (
|
||||
_EXTRA_UI_SETTINGS_FIELDS,
|
||||
ALLOWED_UI_SETTINGS_FIELDS,
|
||||
register_extra_ui_setting,
|
||||
)
|
||||
|
||||
# Snapshot + restore extension registry so the test doesn't leak.
|
||||
original_fields = dict(_EXTRA_UI_SETTINGS_FIELDS)
|
||||
original_allowed = set(ALLOWED_UI_SETTINGS_FIELDS)
|
||||
monkeypatch.setattr(
|
||||
proxy_setting_endpoints, "_EFFECTIVE_UI_SETTINGS_CLASS", None
|
||||
)
|
||||
|
||||
try:
|
||||
register_extra_ui_setting(
|
||||
"test_extension_flag", bool, FieldInfo(default=False)
|
||||
)
|
||||
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
|
||||
response = client.get("/get/ui_settings")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert (
|
||||
data["field_schema"]["description"]
|
||||
== "Configuration for UI-specific flags"
|
||||
)
|
||||
finally:
|
||||
_EXTRA_UI_SETTINGS_FIELDS.clear()
|
||||
_EXTRA_UI_SETTINGS_FIELDS.update(original_fields)
|
||||
ALLOWED_UI_SETTINGS_FIELDS.clear()
|
||||
ALLOWED_UI_SETTINGS_FIELDS.update(original_allowed)
|
||||
proxy_setting_endpoints._EFFECTIVE_UI_SETTINGS_CLASS = None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_role",
|
||||
[
|
||||
|
||||
@ -320,22 +320,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 />
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user