From 513e7a89ac17e5301ceb57fa30e078454cacee7f Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 13:58:11 -0700 Subject: [PATCH] refactor(types): move user request models into litellm.types.proxy.management_endpoints.user_requests (#45813) * refactor(types): move LitellmUserRoles and KeyManagementRoutes into litellm.types.proxy.auth.user_roles Part of LIT-9423 * refactor(types): annotate GenerateRequestBase containers and check_max_budget Part of LIT-9423 * refactor(types): move GenerateRequestBase and LiteLLM_ObjectPermissionBase into litellm.types.proxy.management_endpoints.request_base Part of LIT-9423 * refactor(types): annotate user request metadata and check_user_info validators Part of LIT-9423 * refactor(types): move user request models into litellm.types.proxy.management_endpoints.user_requests Part of LIT-9423 * fix(types): restore main's _types.py content alongside the request_base move Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(sdk): resolve litellm.proxy lazily on attribute access The layer cleanup stopped import litellm from loading any proxy module, so litellm.proxy was no longer bound and litellm.proxy._types.X raised AttributeError. The module __getattr__ now imports litellm.proxy on first access, which keeps import litellm free of proxy._types and fixes the docs and code-quality checks on main * fix(sdk): resolve litellm.proxy lazily on attribute access The layer cleanup stopped import litellm from loading any proxy module, so litellm.proxy was no longer bound and litellm.proxy._types.X raised AttributeError. The module __getattr__ now imports litellm.proxy on first access, which keeps import litellm free of proxy._types and fixes the docs and code-quality checks on main * style(types): merge request_base re-exports into one import block Part of LIT-9423 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/_types.py | 84 ++++--------------- .../internal_user_endpoints.py | 6 +- .../management_endpoints/user_requests.py | 82 ++++++++++++++++++ 3 files changed, 100 insertions(+), 72 deletions(-) create mode 100644 litellm/types/proxy/management_endpoints/user_requests.py diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 4cbf4f15d6d..f43ce4dc211 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1684,41 +1684,12 @@ class ListSkillsRequest(LiteLLMPydanticObjectBase): offset: int | None = 0 -class NewUserRequestTeam(LiteLLMPydanticObjectBase): - team_id: str - max_budget_in_team: float | None = None - user_role: Literal["user", "admin"] = "user" - - -class NewUserRequest(GenerateRequestBase): - max_budget: float | None = None - user_email: str | None = None - user_alias: str | None = None - user_role: ( - Literal[ - LitellmUserRoles.PROXY_ADMIN, - LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, - LitellmUserRoles.INTERNAL_USER, - LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, - ] - | None - ) = None - teams: list[str] | list[NewUserRequestTeam] | None = None - auto_create_key: bool = True # flag used for returning a key as part of the /user/new response - send_invite_email: bool | None = None - sso_user_id: str | None = None - organizations: list[str] | None = None - password: str | None = None - - @field_validator("password") - @classmethod - def password_not_supported(cls, value: str | None) -> str | None: - if value is not None: - raise ValueError( - "password cannot be set via /user/new. Users set their own password through an " - "invitation link (POST /invitation/new)." - ) - return value +from litellm.types.proxy.management_endpoints.user_requests import ( # noqa: E402 # re-export after the definitions above + NewUserRequest as NewUserRequest, # noqa: PLC0414 # public re-export +) +from litellm.types.proxy.management_endpoints.user_requests import ( # noqa: E402 # re-export after the definitions above + NewUserRequestTeam as NewUserRequestTeam, # noqa: PLC0414 # public re-export +) class NewUserResponse(GenerateKeyResponse): @@ -1740,36 +1711,12 @@ class NewUserResponse(GenerateKeyResponse): updated_at: datetime | None = None -class UpdateUserRequestNoUserIDorEmail(GenerateRequestBase): # shared with BulkUpdateUserRequest - # repr=False keeps the plaintext out of management-endpoint alerts, which str() the request model - password: str | None = Field(default=None, repr=False) - spend: float | None = None - metadata: dict[object, object] | None = None - user_alias: str | None = None - user_role: ( - Literal[ - LitellmUserRoles.PROXY_ADMIN, - LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, - LitellmUserRoles.INTERNAL_USER, - LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, - ] - | None - ) = None - max_budget: float | None = None - - -class UpdateUserRequest(UpdateUserRequestNoUserIDorEmail): - # Note: the defaults of all Params here MUST BE NONE - # else they will get overwritten - user_id: str | None = None - user_email: str | None = None - - @model_validator(mode="before") - @classmethod - def check_user_info(cls, values: Mapping[str, object]) -> Mapping[str, object]: - if values.get("user_id") is None and values.get("user_email") is None: - raise ValueError("Either user id or user email must be provided") - return values +from litellm.types.proxy.management_endpoints.user_requests import ( # noqa: E402 # re-export after the definitions above + UpdateUserRequest as UpdateUserRequest, # noqa: PLC0414 # public re-export +) +from litellm.types.proxy.management_endpoints.user_requests import ( # noqa: E402 # re-export after the definitions above + UpdateUserRequestNoUserIDorEmail as UpdateUserRequestNoUserIDorEmail, # noqa: PLC0414 # public re-export +) class ChangePasswordRequest(LiteLLMPydanticObjectBase): @@ -3632,10 +3579,6 @@ class LiteLLM_UserTableFiltered(LiteLLMBaseModel): # done to avoid exposing sen user_email: str | None = None -class LiteLLM_UserTableWithKeyCount(LiteLLM_UserTable): - key_count: int = 0 - - from litellm.models.access_group import ( # noqa: E402 LiteLLM_AccessGroupTable as LiteLLM_AccessGroupTable, ) @@ -3649,6 +3592,9 @@ from litellm.models.spend_logs import ( # noqa: E402 LiteLLM_SpendLogs as LiteLLM_SpendLogs, ) from litellm.models.tag import LiteLLM_TagTable as LiteLLM_TagTable # noqa: E402 +from litellm.types.proxy.management_endpoints.user_requests import ( # noqa: E402 # re-export after the definitions above + LiteLLM_UserTableWithKeyCount as LiteLLM_UserTableWithKeyCount, # noqa: PLC0414 # public re-export +) AUDIT_ACTIONS = Literal["created", "updated", "deleted", "blocked", "unblocked", "rotated", "kill_switch_fired"] diff --git a/litellm/types/proxy/management_endpoints/internal_user_endpoints.py b/litellm/types/proxy/management_endpoints/internal_user_endpoints.py index 1fd3e7dca53..febd4485d10 100644 --- a/litellm/types/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/types/proxy/management_endpoints/internal_user_endpoints.py @@ -4,14 +4,14 @@ from typing import Any, Final, Literal from pydantic import ConfigDict, Field, field_validator from typing_extensions import ReadOnly, TypedDict -from litellm.proxy._types import ( +from litellm.types.llms.base import LiteLLMBaseModel +from litellm.types.proxy.management_endpoints.management_v1 import ResourceResponse +from litellm.types.proxy.management_endpoints.user_requests import ( LiteLLM_UserTableWithKeyCount, NewUserRequest, UpdateUserRequest, UpdateUserRequestNoUserIDorEmail, ) -from litellm.types.llms.base import LiteLLMBaseModel -from litellm.types.proxy.management_endpoints.management_v1 import ResourceResponse MAX_BULK_DELETE_USERS: Final = 500 diff --git a/litellm/types/proxy/management_endpoints/user_requests.py b/litellm/types/proxy/management_endpoints/user_requests.py new file mode 100644 index 00000000000..49dda5995bc --- /dev/null +++ b/litellm/types/proxy/management_endpoints/user_requests.py @@ -0,0 +1,82 @@ +from collections.abc import Mapping +from typing import Literal + +from pydantic import Field, field_validator, model_validator + +from litellm.models.user import LiteLLM_UserTable +from litellm.types.proxy.auth.user_roles import LitellmUserRoles +from litellm.types.proxy.management_endpoints.request_base import GenerateRequestBase +from litellm.types.utils import LiteLLMPydanticObjectBase + + +class NewUserRequestTeam(LiteLLMPydanticObjectBase): + team_id: str + max_budget_in_team: float | None = None + user_role: Literal["user", "admin"] = "user" + + +class NewUserRequest(GenerateRequestBase): + max_budget: float | None = None + user_email: str | None = None + user_alias: str | None = None + user_role: ( + Literal[ + LitellmUserRoles.PROXY_ADMIN, + LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, + LitellmUserRoles.INTERNAL_USER, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, + ] + | None + ) = None + teams: list[str] | list[NewUserRequestTeam] | None = None + auto_create_key: bool = True # flag used for returning a key as part of the /user/new response + send_invite_email: bool | None = None + sso_user_id: str | None = None + organizations: list[str] | None = None + password: str | None = None + + @field_validator("password") + @classmethod + def password_not_supported(cls, value: str | None) -> str | None: + if value is not None: + raise ValueError( + "password cannot be set via /user/new. Users set their own password through an " + "invitation link (POST /invitation/new)." + ) + return value + + +class UpdateUserRequestNoUserIDorEmail(GenerateRequestBase): # shared with BulkUpdateUserRequest + # repr=False keeps the plaintext out of management-endpoint alerts, which str() the request model + password: str | None = Field(default=None, repr=False) + spend: float | None = None + metadata: dict[object, object] | None = None + user_alias: str | None = None + user_role: ( + Literal[ + LitellmUserRoles.PROXY_ADMIN, + LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, + LitellmUserRoles.INTERNAL_USER, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, + ] + | None + ) = None + max_budget: float | None = None + + +class UpdateUserRequest(UpdateUserRequestNoUserIDorEmail): + # Note: the defaults of all Params here MUST BE NONE + # else they will get overwritten + user_id: str | None = None + user_email: str | None = None + + @model_validator(mode="before") + @classmethod + def check_user_info(cls, values: Mapping[str, object]) -> Mapping[str, object]: + if values.get("user_id") is None and values.get("user_email") is None: + raise ValueError("Either user id or user email must be provided") + return values + + +class LiteLLM_UserTableWithKeyCount(LiteLLM_UserTable): + key_count: int = 0