diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 9046d52228..522e85632d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1067,6 +1067,7 @@ class KeyRequestBase(GenerateRequestBase): key: Optional[str] = None budget_id: Optional[str] = None tags: Optional[List[str]] = None + disable_global_guardrails: Optional[bool] = None enforced_params: Optional[List[str]] = None allowed_routes: Optional[list] = [] allowed_passthrough_routes: Optional[list] = None @@ -1832,6 +1833,7 @@ class NewTeamRequest(TeamBase): prompts: Optional[List[str]] = None object_permission: Optional[LiteLLM_ObjectPermissionBase] = None allowed_passthrough_routes: Optional[list] = None + disable_global_guardrails: Optional[bool] = None secret_manager_settings: Optional[dict] = None model_rpm_limit: Optional[Dict[str, int]] = None rpm_limit_type: Optional[ @@ -1900,6 +1902,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase): guardrails: Optional[List[str]] = None policies: Optional[List[str]] = None object_permission: Optional[LiteLLM_ObjectPermissionBase] = None + disable_global_guardrails: Optional[bool] = None team_member_budget: Optional[float] = None team_member_budget_duration: Optional[str] = None team_member_rpm_limit: Optional[int] = None @@ -4281,6 +4284,7 @@ LiteLLM_ManagementEndpoint_MetadataFields = [ ] LiteLLM_ManagementEndpoint_MetadataFields_Premium = [ + "disable_global_guardrails", "guardrails", "policies", "tags", diff --git a/tests/proxy_admin_ui_tests/test_key_management.py b/tests/proxy_admin_ui_tests/test_key_management.py index 933c75e4d3..4c5a045509 100644 --- a/tests/proxy_admin_ui_tests/test_key_management.py +++ b/tests/proxy_admin_ui_tests/test_key_management.py @@ -853,6 +853,18 @@ def test_personal_key_generation_check(): {"tags": ["old_tag"]}, {"metadata": {"tags": ["old_tag"], "enforced_params": ["metadata.tags"]}}, ), + ( + {"disable_global_guardrails": True}, + {}, + {}, + {"metadata": {"disable_global_guardrails": True}}, + ), + ( + {"disable_global_guardrails": False}, + {}, + {"disable_global_guardrails": True}, + {"metadata": {"disable_global_guardrails": False}}, + ), ], ) def test_prepare_metadata_fields( 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 7d3f077daf..b590a7dc04 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -439,6 +439,12 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp // Update the formValues with the final metadata formValues.metadata = JSON.stringify(metadata); + // disable_global_guardrails is premium-gated server-side; only send it when enabled + // so non-premium key creation isn't blocked by that gate. + if (!formValues.disable_global_guardrails) { + delete formValues.disable_global_guardrails; + } + // Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission format if (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) { formValues.object_permission = { diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 60b7e31d47..2de40925b1 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -34,8 +34,15 @@ interface KeyInfoViewProps { backButtonText?: string; } -// Must stay in sync with LiteLLM_ManagementEndpoint_MetadataFields_Premium -// in litellm/proxy/_types.py — limited to fields the key-edit form submits. +// Premium fields (from LiteLLM_ManagementEndpoint_MetadataFields_Premium in +// litellm/proxy/_types.py) that the key-edit form submits as arrays/strings, where +// "empty" means "unset". The loop below drops them when they're empty-and-were-empty +// so a non-premium edit of unrelated fields doesn't trip the server's premium gate. +// +// Boolean premium fields (e.g. disable_global_guardrails) do NOT belong here: false is +// a real value, not "empty", so isEmptyValue(false) is false and the loop would never +// drop it — we'd resend false on every edit and trip the gate. Booleans get their own +// "send only when changed" guard instead (see disable_global_guardrails below). const PREMIUM_METADATA_FIELDS = [ "policies", "guardrails", @@ -174,6 +181,15 @@ export default function KeyInfoView({ } } + // disable_global_guardrails is premium-gated server-side; only send it when it + // changed so a non-premium edit of unrelated fields isn't blocked by that gate. + const previousDisableGlobalGuardrails = Boolean( + (currentKeyData.metadata as Record | undefined)?.disable_global_guardrails, + ); + if (Boolean(formValues.disable_global_guardrails) === previousDisableGlobalGuardrails) { + delete formValues.disable_global_guardrails; + } + // Handle max budget empty string formValues.max_budget = mapEmptyStringToNull(formValues.max_budget);