From 85c751c6b5096c627456aaa102fde171df02f29c Mon Sep 17 00:00:00 2001 From: joshbouy <15766231+joshbouy@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:20:20 -0700 Subject: [PATCH] feat(guardrails): resolve default_on from env var via os.environ/ prefix Allow guardrail configs to set default_on to an os.environ/ reference that resolves to a bool via get_secret_bool, mirroring how api_key and api_base already accept os.environ/ references in the same loader. The use case is keeping a single central config.yaml for the litellm proxy across kubernetes environments while letting each environment drive whether a guardrail is default_on through its own env var, instead of forking the config per deployment. The field's type is unchanged (Optional[bool]); plain true/false values pass through untouched. Resolution only triggers on the os.environ/ prefix, so a quoted "true" still coerces normally and a typo like ture still fails loudly at startup rather than silently disabling the guardrail. An unset referenced variable falls back to False, matching LitellmParams' existing default. --- .../proxy/guardrails/guardrail_registry.py | 10 ++- .../guardrails/test_guardrail_registry.py | 90 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/guardrails/guardrail_registry.py b/litellm/proxy/guardrails/guardrail_registry.py index b99ea8f14a0..64e86103dbd 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -22,7 +22,7 @@ from litellm.proxy.guardrails.guardrail_hooks.grayswan import ( from litellm.proxy.types_utils.utils import get_instance_fn from litellm.proxy.utils import PrismaClient from litellm.repositories.table_repositories import GuardrailsRepository -from litellm.secret_managers.main import get_secret +from litellm.secret_managers.main import get_secret, get_secret_bool from litellm.types.guardrails import ( Guardrail, GuardrailEventHooks, @@ -444,6 +444,14 @@ class InMemoryGuardrailHandler: verbose_proxy_logger.debug("litellm_params= %s", litellm_params_data) if isinstance(litellm_params_data, dict): + default_on_raw = litellm_params_data.get("default_on") + if isinstance(default_on_raw, str) and default_on_raw.startswith( + "os.environ/" + ): + litellm_params_data = { + **litellm_params_data, + "default_on": get_secret_bool(default_on_raw), + } litellm_params = LitellmParams(**litellm_params_data) else: litellm_params = litellm_params_data diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py b/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py index 0ef9ad857f9..df071892c0f 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py @@ -349,3 +349,93 @@ def test_repeated_db_sync_does_not_accumulate_runner_instances(): finally: for cb_list, snapshot in zip(lists, snapshots): cb_list[:] = snapshot + + +def _guardrail_with_default_on(raw_default_on) -> Guardrail: + return Guardrail( + guardrail_id="default-on-env-var", + guardrail_name="default-on-env-var-guard", + litellm_params={ + "guardrail": "default_on_env_var_fake", + "mode": "pre_call", + "default_on": raw_default_on, + }, + ) + + +def _init_handler_with_fake_initializer(captured, default_on_value): + from unittest.mock import patch + + from litellm.proxy.guardrails import guardrail_registry as registry_module + + def fake_initializer(params, guardrail): + captured["default_on"] = params.default_on + return CustomGuardrail( + guardrail_name=guardrail.get("guardrail_name", ""), + event_hook=params.mode, + default_on=params.default_on, + ) + + with patch.dict( + registry_module.guardrail_initializer_registry, + {"default_on_env_var_fake": fake_initializer}, + ): + handler = registry_module.InMemoryGuardrailHandler() + handler.initialize_guardrail( + guardrail=_guardrail_with_default_on(default_on_value), source="config" + ) + return handler + + +def test_initialize_guardrail_resolves_default_on_env_var_true(monkeypatch): + captured: dict = {} + monkeypatch.setenv("LITELLM_TEST_DEFAULT_ON_TRUE", "true") + handler = _init_handler_with_fake_initializer( + captured, "os.environ/LITELLM_TEST_DEFAULT_ON_TRUE" + ) + + assert captured["default_on"] is True + callback = handler.guardrail_id_to_custom_guardrail["default-on-env-var"] + assert isinstance(callback, CustomGuardrail) + assert callback.default_on is True + + +def test_initialize_guardrail_resolves_default_on_env_var_false(monkeypatch): + captured: dict = {} + monkeypatch.setenv("LITELLM_TEST_DEFAULT_ON_FALSE", "false") + handler = _init_handler_with_fake_initializer( + captured, "os.environ/LITELLM_TEST_DEFAULT_ON_FALSE" + ) + + assert captured["default_on"] is False + callback = handler.guardrail_id_to_custom_guardrail["default-on-env-var"] + assert callback.default_on is False + + +def test_initialize_guardrail_unsets_default_on_env_var_falls_back_false(monkeypatch): + captured: dict = {} + monkeypatch.delenv("LITELLM_TEST_DEFAULT_ON_MISSING", raising=False) + handler = _init_handler_with_fake_initializer( + captured, "os.environ/LITELLM_TEST_DEFAULT_ON_MISSING" + ) + + assert captured["default_on"] is False + callback = handler.guardrail_id_to_custom_guardrail["default-on-env-var"] + assert callback.default_on is False + + +def test_initialize_guardrail_non_prefixed_bool_string_not_env_resolved(monkeypatch): + monkeypatch.setenv("true", "false") + captured: dict = {} + _init_handler_with_fake_initializer(captured, "true") + + assert captured["default_on"] is True + + +def test_initialize_guardrail_plain_bool_default_on_still_works(): + captured: dict = {} + handler = _init_handler_with_fake_initializer(captured, True) + + assert captured["default_on"] is True + callback = handler.guardrail_id_to_custom_guardrail["default-on-env-var"] + assert callback.default_on is True