From e1fa0ba13aa247c0456ab2436c81585fed35a34e Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 31 Mar 2026 16:35:02 -0700 Subject: [PATCH 1/3] feat: add key_rotation_email field to LiteLLM_VerificationToken schema --- litellm/proxy/schema.prisma | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 46be6b31e1f..e136fbaa6be 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -387,6 +387,7 @@ model LiteLLM_VerificationToken { rotation_interval String? // How often to rotate (e.g., "30d", "90d") last_rotation_at DateTime? // When this key was last rotated key_rotation_at DateTime? // When this key should next be rotated + key_rotation_email String? // Override email for rotation notifications (useful for service accounts) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) litellm_project_table LiteLLM_ProjectTable? @relation(fields: [project_id], references: [project_id]) @@ -480,7 +481,8 @@ model LiteLLM_DeletedVerificationToken { rotation_interval String? last_rotation_at DateTime? key_rotation_at DateTime? - + key_rotation_email String? + // Deletion metadata deleted_at DateTime @default(now()) @map("deleted_at") deleted_by String? @map("deleted_by") // User who deleted the key From 65a872781455e488e1cb2a6e6f10a80de9e4ada8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 31 Mar 2026 16:38:02 -0700 Subject: [PATCH 2/3] feat: add key_rotation_email field to API types and email notification hooks --- docs/my-website/docs/proxy/virtual_keys.md | 25 ++++- .../send_emails/base_email.py | 3 +- .../types/enterprise_callbacks/send_emails.py | 1 + litellm/proxy/_types.py | 104 ++++++++++-------- .../proxy/hooks/key_management_event_hooks.py | 16 ++- pyrightconfig.json | 1 + .../KeyLifecycleSettings.tsx | 24 +++- .../organisms/create_key_button.tsx | 6 + .../components/templates/key_edit_view.tsx | 10 ++ 9 files changed, 132 insertions(+), 58 deletions(-) diff --git a/docs/my-website/docs/proxy/virtual_keys.md b/docs/my-website/docs/proxy/virtual_keys.md index c74aa75ff4a..3c149619ee3 100644 --- a/docs/my-website/docs/proxy/virtual_keys.md +++ b/docs/my-website/docs/proxy/virtual_keys.md @@ -596,14 +596,32 @@ curl 'http://0.0.0.0:4000/key/generate' \ }' ``` +**Set a custom rotation notification email (useful for service accounts)** + +By default, rotation emails go to the key owner. Use `key_rotation_email` to send notifications to a specific address instead — useful when the key is owned by a service account with no real inbox. + +```bash +curl 'http://0.0.0.0:4000/key/generate' \ + -H 'Authorization: Bearer ' \ + -H 'Content-Type: application/json' \ + -d '{ + "models": ["gpt-4o"], + "auto_rotate": true, + "rotation_interval": "30d", + "key_rotation_email": "platform-alerts@yourcompany.com" + }' +``` + **LiteLLM UI** On the LiteLLM UI, Navigate to the Keys page and click on `Generate Key` > `Key Lifecycle` > `Enable Auto Rotation` - +When auto-rotation is enabled, a **Rotation Notification Email** field appears. Leave it empty to use the key owner's email, or enter a specific address (e.g. a team alias or alert inbox). + **Valid rotation_interval formats:** - `"30s"` - 30 seconds - `"30m"` - 30 minutes @@ -622,7 +640,8 @@ curl 'http://0.0.0.0:4000/key/update' \ -d '{ "key": "sk-existing-key", "auto_rotate": true, - "rotation_interval": "90d" + "rotation_interval": "90d", + "key_rotation_email": "platform-alerts@yourcompany.com" }' ``` @@ -630,7 +649,7 @@ curl 'http://0.0.0.0:4000/key/update' \ On the LiteLLM UI, Navigate to the Keys page. Select the key you want to update and click on `Edit Settings` > `Auto-Rotation Settings` - diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index 7a77898b160..70872044e41 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -158,7 +158,8 @@ class BaseEmailLogger(CustomLogger): """ email_params = await self._get_email_params( user_id=send_key_rotated_email_event.user_id, - user_email=send_key_rotated_email_event.user_email, + user_email=send_key_rotated_email_event.key_rotation_email + or send_key_rotated_email_event.user_email, email_event=EmailEvent.virtual_key_rotated, event_message=send_key_rotated_email_event.event_message, ) diff --git a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py index 380b0a6facb..4f7b766cfc0 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -26,6 +26,7 @@ class SendKeyCreatedEmailEvent(WebhookEvent): class SendKeyRotatedEmailEvent(WebhookEvent): virtual_key: str key_alias: Optional[str] = None + key_rotation_email: Optional[str] = None """ The virtual key that was rotated this will be sk-123xxx, since we will be emailing this to the user to start using the new key diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 8faf36df4c6..8736a4e7613 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -884,9 +884,9 @@ class GenerateRequestBase(LiteLLMPydanticObjectBase): allowed_cache_controls: Optional[list] = [] config: Optional[dict] = {} permissions: Optional[dict] = {} - model_max_budget: Optional[ - dict - ] = {} # {"gpt-4": 5.0, "gpt-3.5-turbo": 5.0}, defaults to {} + model_max_budget: Optional[dict] = ( + {} + ) # {"gpt-4": 5.0, "gpt-3.5-turbo": 5.0}, defaults to {} model_config = ConfigDict(protected_namespaces=()) model_rpm_limit: Optional[dict] = None @@ -954,6 +954,10 @@ class GenerateKeyRequest(KeyRequestBase): default=None, description="How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True", ) + key_rotation_email: Optional[str] = Field( + default=None, + description="Email to notify when key is rotated. Overrides key owner email. Useful for service accounts.", + ) organization_id: Optional[str] = None project_id: Optional[str] = None @@ -1008,6 +1012,7 @@ class UpdateKeyRequest(KeyRequestBase): temp_budget_expiry: Optional[datetime] = None auto_rotate: Optional[bool] = None rotation_interval: Optional[str] = None + key_rotation_email: Optional[str] = None organization_id: Optional[str] = None @model_validator(mode="after") @@ -1028,9 +1033,9 @@ class RegenerateKeyRequest(GenerateKeyRequest): spend: Optional[float] = None metadata: Optional[dict] = None new_master_key: Optional[str] = None - grace_period: Optional[ - str - ] = None # Duration to keep old key valid (e.g. "24h", "2d"); None = immediate revoke + grace_period: Optional[str] = ( + None # Duration to keep old key valid (e.g. "24h", "2d"); None = immediate revoke + ) class ResetSpendRequest(LiteLLMPydanticObjectBase): @@ -1540,12 +1545,12 @@ class NewCustomerRequest(BudgetNewRequest): blocked: bool = False # allow/disallow requests for this end-user budget_id: Optional[str] = None # give either a budget_id or max_budget spend: Optional[float] = None - allowed_model_region: Optional[ - AllowedModelRegion - ] = None # require all user requests to use models in this specific region - default_model: Optional[ - str - ] = None # if no equivalent model in allowed region - default all requests to this model + allowed_model_region: Optional[AllowedModelRegion] = ( + None # require all user requests to use models in this specific region + ) + default_model: Optional[str] = ( + None # if no equivalent model in allowed region - default all requests to this model + ) object_permission: Optional[LiteLLM_ObjectPermissionBase] = None @model_validator(mode="before") @@ -1568,12 +1573,12 @@ class UpdateCustomerRequest(LiteLLMPydanticObjectBase): blocked: bool = False # allow/disallow requests for this end-user max_budget: Optional[float] = None budget_id: Optional[str] = None # give either a budget_id or max_budget - allowed_model_region: Optional[ - AllowedModelRegion - ] = None # require all user requests to use models in this specific region - default_model: Optional[ - str - ] = None # if no equivalent model in allowed region - default all requests to this model + allowed_model_region: Optional[AllowedModelRegion] = ( + None # require all user requests to use models in this specific region + ) + default_model: Optional[str] = ( + None # if no equivalent model in allowed region - default all requests to this model + ) object_permission: Optional[LiteLLM_ObjectPermissionBase] = None @@ -1663,15 +1668,15 @@ class NewTeamRequest(TeamBase): ] = None # raise an error if 'guaranteed_throughput' is set and we're overallocating tpm model_tpm_limit: Optional[Dict[str, int]] = None - team_member_budget: Optional[ - float - ] = None # allow user to set a budget for all team members - team_member_rpm_limit: Optional[ - int - ] = None # allow user to set RPM limit for all team members - team_member_tpm_limit: Optional[ - int - ] = None # allow user to set TPM limit for all team members + team_member_budget: Optional[float] = ( + None # allow user to set a budget for all team members + ) + team_member_rpm_limit: Optional[int] = ( + None # allow user to set RPM limit for all team members + ) + team_member_tpm_limit: Optional[int] = ( + None # allow user to set TPM limit for all team members + ) team_member_key_duration: Optional[str] = None # e.g. "1d", "1w", "1m" team_member_budget_duration: Optional[str] = None # e.g. "30d", "1mo" allowed_vector_store_indexes: Optional[List[AllowedVectorStoreIndexItem]] = None @@ -1768,9 +1773,9 @@ class BlockKeyRequest(LiteLLMPydanticObjectBase): class AddTeamCallback(LiteLLMPydanticObjectBase): callback_name: str - callback_type: Optional[ - Literal["success", "failure", "success_and_failure"] - ] = "success_and_failure" + callback_type: Optional[Literal["success", "failure", "success_and_failure"]] = ( + "success_and_failure" + ) callback_vars: Dict[str, str] @model_validator(mode="before") @@ -2110,9 +2115,9 @@ class ConfigList(LiteLLMPydanticObjectBase): stored_in_db: Optional[bool] field_default_value: Any premium_field: bool = False - nested_fields: Optional[ - List[FieldDetail] - ] = None # For nested dictionary or Pydantic fields + nested_fields: Optional[List[FieldDetail]] = ( + None # For nested dictionary or Pydantic fields + ) class UserHeaderMapping(LiteLLMPydanticObjectBase): @@ -2360,6 +2365,9 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase): rotation_interval: Optional[str] = None # How often to rotate (e.g., "30d", "90d") last_rotation_at: Optional[datetime] = None # When this key was last rotated key_rotation_at: Optional[datetime] = None # When this key should next be rotated + key_rotation_email: Optional[str] = ( + None # Override email for rotation notifications + ) router_settings: Optional[dict] = None model_config = ConfigDict(protected_namespaces=()) @@ -2470,9 +2478,9 @@ class UserAPIKeyAuth( user_max_budget: Optional[float] = None request_route: Optional[str] = None user: Optional[Any] = None # Expanded user object when expand=user is used - created_by_user: Optional[ - Any - ] = None # Expanded created_by user when expand=user is used + created_by_user: Optional[Any] = ( + None # Expanded created_by user when expand=user is used + ) end_user_object_permission: Optional[LiteLLM_ObjectPermissionTable] = None # Decoded upstream IdP claims (groups, roles, etc.) propagated by JWT auth machinery # and forwarded into outbound tokens by guardrails such as MCPJWTSigner. @@ -2611,9 +2619,9 @@ class LiteLLM_OrganizationMembershipTable(LiteLLMPydanticObjectBase): budget_id: Optional[str] = None created_at: datetime updated_at: datetime - user: Optional[ - Any - ] = None # You might want to replace 'Any' with a more specific type if available + user: Optional[Any] = ( + None # You might want to replace 'Any' with a more specific type if available + ) litellm_budget_table: Optional[LiteLLM_BudgetTable] = None user_email: Optional[str] = None @@ -3764,9 +3772,9 @@ class TeamModelDeleteRequest(BaseModel): # Organization Member Requests class OrganizationMemberAddRequest(OrgMemberAddRequest): organization_id: str - max_budget_in_organization: Optional[ - float - ] = None # Users max budget within the organization + max_budget_in_organization: Optional[float] = ( + None # Users max budget within the organization + ) class OrganizationMemberDeleteRequest(MemberDeleteRequest): @@ -4017,9 +4025,9 @@ class ProviderBudgetResponse(LiteLLMPydanticObjectBase): Maps provider names to their budget configs. """ - providers: Dict[ - str, ProviderBudgetResponseObject - ] = {} # Dictionary mapping provider names to their budget configurations + providers: Dict[str, ProviderBudgetResponseObject] = ( + {} + ) # Dictionary mapping provider names to their budget configurations class ProxyStateVariables(TypedDict): @@ -4163,9 +4171,9 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): enforce_rbac: bool = False roles_jwt_field: Optional[str] = None # v2 on role mappings role_mappings: Optional[List[RoleMapping]] = None - object_id_jwt_field: Optional[ - str - ] = None # can be either user / team, inferred from the role mapping + object_id_jwt_field: Optional[str] = ( + None # can be either user / team, inferred from the role mapping + ) scope_mappings: Optional[List[ScopeMapping]] = None enforce_scope_based_access: bool = False enforce_team_based_model_access: bool = False diff --git a/litellm/proxy/hooks/key_management_event_hooks.py b/litellm/proxy/hooks/key_management_event_hooks.py index 2d61203ad51..8d42cac6f41 100644 --- a/litellm/proxy/hooks/key_management_event_hooks.py +++ b/litellm/proxy/hooks/key_management_event_hooks.py @@ -178,6 +178,9 @@ class KeyManagementEventHooks: await KeyManagementEventHooks._send_key_rotated_email( response=response.model_dump(exclude_none=True), existing_key_alias=existing_key_row.key_alias, + key_rotation_email=getattr( + existing_key_row, "key_rotation_email", None + ), ) except Exception as e: verbose_proxy_logger.warning(f"Failed to send key rotated email: {e}") @@ -364,10 +367,10 @@ class KeyManagementEventHooks: if key.key_alias is not None: team_id = getattr(key, "team_id", None) if team_id not in team_settings_cache: - team_settings_cache[ - team_id - ] = await KeyManagementEventHooks._get_secret_manager_optional_params( - team_id + team_settings_cache[team_id] = ( + await KeyManagementEventHooks._get_secret_manager_optional_params( + team_id + ) ) optional_params = team_settings_cache[team_id] await litellm.secret_manager_client.async_delete_secret( @@ -542,7 +545,9 @@ class KeyManagementEventHooks: @staticmethod async def _send_key_rotated_email( - response: dict, existing_key_alias: Optional[str] + response: dict, + existing_key_alias: Optional[str], + key_rotation_email: Optional[str] = None, ): """ Send key rotated email if email sending is enabled. @@ -589,6 +594,7 @@ class KeyManagementEventHooks: user_id=response.get("user_id", None), team_id=response.get("team_id", "Default Team"), key_alias=response.get("key_alias", existing_key_alias), + key_rotation_email=key_rotation_email, ) ########################## diff --git a/pyrightconfig.json b/pyrightconfig.json index f930e44d305..f40605b7ea0 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,6 +1,7 @@ { "ignore": [], "exclude": ["**/node_modules", "**/__pycache__", "litellm/types/utils.py", "litellm/proxy/_types.py"], + "extraPaths": ["enterprise"], "reportMissingImports": false, "reportPrivateImportUsage": false } diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx index 17d65e1f66a..c1efcde0965 100644 --- a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx +++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx @@ -11,6 +11,8 @@ interface KeyLifecycleSettingsProps { onAutoRotationChange: (enabled: boolean) => void; rotationInterval: string; onRotationIntervalChange: (interval: string) => void; + keyRotationEmail?: string; + onKeyRotationEmailChange?: (email: string) => void; isCreateMode?: boolean; // If true, shows "leave empty to never expire" instead of "-1 to never expire" neverExpire?: boolean; onNeverExpireChange?: (checked: boolean) => void; @@ -22,6 +24,8 @@ const KeyLifecycleSettings: React.FC = ({ onAutoRotationChange, rotationInterval, onRotationIntervalChange, + keyRotationEmail = "", + onKeyRotationEmailChange, isCreateMode = false, neverExpire = false, onNeverExpireChange, @@ -169,9 +173,27 @@ const KeyLifecycleSettings: React.FC = ({ )} + {autoRotationEnabled && ( +
+ + onKeyRotationEmailChange?.(val)} + /> +
+ )} + {autoRotationEnabled && (
- When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated + When rotation occurs, a notification with the new key will be sent to the configured email (or the key owner if none set). The old key will be deactivated after a brief grace period.
)} diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 76888262e5a..d2d4b10c579 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -200,6 +200,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({}); const [autoRotationEnabled, setAutoRotationEnabled] = useState(false); const [rotationInterval, setRotationInterval] = useState("30d"); + const [keyRotationEmail, setKeyRotationEmail] = useState(""); const [routerSettings, setRouterSettings] = useState(null); const [routerSettingsKey, setRouterSettingsKey] = useState(0); const [agentsList, setAgentsList] = useState<{ agent_id: string; agent_name: string }[]>([]); @@ -424,6 +425,9 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp if (autoRotationEnabled) { formValues.auto_rotate = true; formValues.rotation_interval = rotationInterval; + if (keyRotationEmail.trim()) { + formValues.key_rotation_email = keyRotationEmail.trim(); + } } // Handle duration field for key expiry - convert empty string to null @@ -1532,6 +1536,8 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp onAutoRotationChange={setAutoRotationEnabled} rotationInterval={rotationInterval} onRotationIntervalChange={setRotationInterval} + keyRotationEmail={keyRotationEmail} + onKeyRotationEmailChange={setKeyRotationEmail} isCreateMode={true} /> diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index cf431d10245..6f12290307c 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -101,6 +101,7 @@ export function KeyEditView({ const [selectedOrganizationId, setSelectedOrganizationId] = useState(keyData.organization_id || null); const [autoRotationEnabled, setAutoRotationEnabled] = useState(keyData.auto_rotate || false); const [rotationInterval, setRotationInterval] = useState(keyData.rotation_interval || ""); + const [keyRotationEmail, setKeyRotationEmail] = useState((keyData as any).key_rotation_email || ""); const [neverExpire, setNeverExpire] = useState(!keyData.expires); const [isKeySaving, setIsKeySaving] = useState(false); const { data: organizations, isLoading: isOrganizationsLoading } = useOrganizations(); @@ -238,6 +239,10 @@ export function KeyEditView({ } }, [rotationInterval, form]); + useEffect(() => { + form.setFieldValue("key_rotation_email", keyRotationEmail || null); + }, [keyRotationEmail, form]); + // Fetch tags for selector useEffect(() => { const fetchTags = async () => { @@ -707,6 +712,8 @@ export function KeyEditView({ onAutoRotationChange={setAutoRotationEnabled} rotationInterval={rotationInterval} onRotationIntervalChange={setRotationInterval} + keyRotationEmail={keyRotationEmail} + onKeyRotationEmailChange={setKeyRotationEmail} neverExpire={neverExpire} onNeverExpireChange={setNeverExpire} /> @@ -732,6 +739,9 @@ export function KeyEditView({ +
From 8ae288b7770bd8cd6881786ee92871fd55c2c516 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 31 Mar 2026 16:48:35 -0700 Subject: [PATCH 3/3] fix: add key_rotation_email to generate_key_helper_fn, KeyResponse type, and SQL migration --- .../migration.sql | 7 ++++ .../key_management_endpoints.py | 42 ++++++++++--------- .../components/key_team_helpers/key_list.tsx | 1 + .../components/templates/key_edit_view.tsx | 2 +- 4 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260331000000_add_key_rotation_email/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260331000000_add_key_rotation_email/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260331000000_add_key_rotation_email/migration.sql new file mode 100644 index 00000000000..6b93050dbdc --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260331000000_add_key_rotation_email/migration.sql @@ -0,0 +1,7 @@ +-- Add key_rotation_email to LiteLLM_VerificationToken and LiteLLM_DeletedVerificationToken +-- AlterTable +ALTER TABLE "LiteLLM_VerificationToken" + ADD COLUMN IF NOT EXISTS "key_rotation_email" TEXT; + +ALTER TABLE "LiteLLM_DeletedVerificationToken" + ADD COLUMN IF NOT EXISTS "key_rotation_email" TEXT; diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 831922ec3f9..fa36d03fcde 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -732,9 +732,9 @@ async def _common_key_generation_helper( # noqa: PLR0915 request_type="key", **data_json, table_name="key" ) - response[ - "soft_budget" - ] = data.soft_budget # include the user-input soft budget in the response + response["soft_budget"] = ( + data.soft_budget + ) # include the user-input soft budget in the response response = GenerateKeyResponse(**response) @@ -2783,6 +2783,7 @@ async def generate_key_helper_fn( # noqa: PLR0915 object_permission: Optional[LiteLLM_ObjectPermissionBase] = None, auto_rotate: Optional[bool] = None, rotation_interval: Optional[str] = None, + key_rotation_email: Optional[str] = None, router_settings: Optional[dict] = None, access_group_ids: Optional[list] = None, ): @@ -2913,6 +2914,9 @@ async def generate_key_helper_fn( # noqa: PLR0915 rotation_interval=rotation_interval, ) + if key_rotation_email is not None: + key_data["key_rotation_email"] = key_rotation_email + if ( get_secret("DISABLE_KEY_NAME", False) is True ): # allow user to disable storing abbreviated key name (shown in UI, to help figure out which key spent how much) @@ -3175,10 +3179,10 @@ async def delete_verification_tokens( try: if prisma_client: tokens = [_hash_token_if_needed(token=key) for key in tokens] - _keys_being_deleted: List[ - LiteLLM_VerificationToken - ] = await prisma_client.db.litellm_verificationtoken.find_many( - where={"token": {"in": tokens}} + _keys_being_deleted: List[LiteLLM_VerificationToken] = ( + await prisma_client.db.litellm_verificationtoken.find_many( + where={"token": {"in": tokens}} + ) ) if len(_keys_being_deleted) == 0: @@ -3378,9 +3382,9 @@ async def _rotate_master_key( # noqa: PLR0915 from litellm.proxy.proxy_server import proxy_config try: - models: Optional[ - List - ] = await prisma_client.db.litellm_proxymodeltable.find_many() + models: Optional[List] = ( + await prisma_client.db.litellm_proxymodeltable.find_many() + ) except Exception: models = None # 2. process model table @@ -4020,11 +4024,11 @@ async def validate_key_list_check( param="user_id", code=status.HTTP_403_FORBIDDEN, ) - complete_user_info_db_obj: Optional[ - BaseModel - ] = await prisma_client.db.litellm_usertable.find_unique( - where={"user_id": user_api_key_dict.user_id}, - include={"organization_memberships": True}, + complete_user_info_db_obj: Optional[BaseModel] = ( + await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": user_api_key_dict.user_id}, + include={"organization_memberships": True}, + ) ) if complete_user_info_db_obj is None: @@ -4107,10 +4111,10 @@ async def _fetch_user_team_objects( if complete_user_info is None or not complete_user_info.teams: return [] - teams: Optional[ - List[BaseModel] - ] = await prisma_client.db.litellm_teamtable.find_many( - where={"team_id": {"in": complete_user_info.teams}} + teams: Optional[List[BaseModel]] = ( + await prisma_client.db.litellm_teamtable.find_many( + where={"team_id": {"in": complete_user_info.teams}} + ) ) if teams is None: return [] diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index a681e438cd1..9eb77af5b2f 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -93,6 +93,7 @@ export interface KeyResponse { access_group_ids?: string[]; auto_rotate?: boolean; rotation_interval?: string; + key_rotation_email?: string; last_rotation_at?: string; key_rotation_at?: string; next_rotation_at?: string; diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 6f12290307c..a338ac0bbbe 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -101,7 +101,7 @@ export function KeyEditView({ const [selectedOrganizationId, setSelectedOrganizationId] = useState(keyData.organization_id || null); const [autoRotationEnabled, setAutoRotationEnabled] = useState(keyData.auto_rotate || false); const [rotationInterval, setRotationInterval] = useState(keyData.rotation_interval || ""); - const [keyRotationEmail, setKeyRotationEmail] = useState((keyData as any).key_rotation_email || ""); + const [keyRotationEmail, setKeyRotationEmail] = useState(keyData.key_rotation_email || ""); const [neverExpire, setNeverExpire] = useState(!keyData.expires); const [isKeySaving, setIsKeySaving] = useState(false); const { data: organizations, isLoading: isOrganizationsLoading } = useOrganizations();