fix(guardrails): persist disable_global_guardrails on keys (#29233)

* fix(guardrails): restore disable_global_guardrails persistence for keys

The per-key/team "Disable Global Guardrails" toggle silently stopped
working after #17042, which removed `disable_global_guardrails` from the
key/team request models and from the premium metadata allowlist. Without
those, the UI's top-level field was dropped by pydantic and never folded
into key `metadata`, so the runtime gate always read False and global
default_on guardrails kept running.

Restore the request-model fields (KeyRequestBase, NewTeamRequest,
UpdateTeamRequest) and the `LiteLLM_ManagementEndpoint_MetadataFields_Premium`
entry so the flag is promoted into metadata again. Because the key edit
form always submits the flag (false by default), guard the UI so it is
only sent when it actually changed (edit) or is enabled (create) — this
keeps the premium gate on enabling intact while not 403-ing non-premium
users who edit unrelated key fields, mirroring how guardrails/tags are
already stripped.

* test(guardrails): cover disable_global_guardrails toggle-off + clarify premium field comment

Add a prepare_metadata_fields case asserting `disable_global_guardrails: False`
overwrites an existing `True`, and rewrite the PREMIUM_METADATA_FIELDS comment to
explain why boolean premium fields are excluded from the empty-value strip loop.
This commit is contained in:
ryan-crabbe-berri 2026-05-28 21:19:04 -07:00 committed by GitHub
parent 01e83e2537
commit 9918a9c78c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 40 additions and 2 deletions

View File

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

View File

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

View File

@ -439,6 +439,12 @@ const CreateKey: React.FC<CreateKeyProps> = ({ 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 = {

View File

@ -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<string, unknown> | 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);