diff --git a/litellm/proxy/guardrails/guardrail_registry.py b/litellm/proxy/guardrails/guardrail_registry.py index 1f2c9e0c182..5c9f832cc18 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -384,6 +384,23 @@ class GuardrailRegistry: raise Exception(f"Error getting guardrail from DB: {str(e)}") +def _resolve_env_references(litellm_params: LitellmParams) -> LitellmParams: + """Expand every ``os.environ/NAME`` reference in a guardrail's litellm_params. + + Only ``api_key`` and ``api_base`` used to be expanded, so a guardrail + configured with e.g. ``aws_region_name: os.environ/AWS_REGION`` handed boto3 + the literal string and failed with "Invalid AWS region format". A name that + resolves to nothing becomes None rather than the string "None", so the + downstream client falls back to its own credential chain. + """ + resolved = { + name: get_secret(value) + for name, value in litellm_params.model_dump().items() + if isinstance(value, str) and value.startswith("os.environ/") + } + return litellm_params.model_copy(update=resolved) if resolved else litellm_params + + class InMemoryGuardrailHandler: """ Class that handles initializing guardrails and adding them to the CallbackManager @@ -442,11 +459,7 @@ class InMemoryGuardrailHandler: lakera_category_thresholds = LakeraCategoryThresholds(**litellm_params_data["category_thresholds"]) litellm_params.category_thresholds = lakera_category_thresholds - if litellm_params.api_key and litellm_params.api_key.startswith("os.environ/"): - litellm_params.api_key = str(get_secret(litellm_params.api_key)) - - if litellm_params.api_base and litellm_params.api_base.startswith("os.environ/"): - litellm_params.api_base = str(get_secret(litellm_params.api_base)) + litellm_params = _resolve_env_references(litellm_params) guardrail_type = litellm_params.guardrail diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py b/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py index 26feddadf79..6bec0c7f6ca 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py @@ -367,3 +367,37 @@ def test_repeated_db_sync_does_not_accumulate_runner_instances(): finally: for cb_list, snapshot in zip(lists, snapshots): cb_list[:] = snapshot + + +def test_resolve_env_references_expands_every_string_field(monkeypatch): + """Regression: only api_key/api_base used to be expanded, so a guardrail set up + with aws_region_name="os.environ/AWS_REGION" handed boto3 the literal string and + failed with "Invalid AWS region format".""" + from litellm.proxy.guardrails.guardrail_registry import _resolve_env_references + + monkeypatch.setenv("TEST_GUARDRAIL_REGION", "us-east-1") + monkeypatch.setenv("TEST_GUARDRAIL_KEY", "sk-guardrail") + monkeypatch.setenv("TEST_GUARDRAIL_ANALYZER", "https://analyzer.example") + monkeypatch.delenv("TEST_GUARDRAIL_ABSENT", raising=False) + + resolved = _resolve_env_references( + LitellmParams( + guardrail="bedrock", + mode="pre_call", + api_key="os.environ/TEST_GUARDRAIL_KEY", + aws_region_name="os.environ/TEST_GUARDRAIL_REGION", + presidio_analyzer_api_base="os.environ/TEST_GUARDRAIL_ANALYZER", + aws_secret_access_key="os.environ/TEST_GUARDRAIL_ABSENT", + aws_access_key_id="AKIA_LITERAL_VALUE", + ) + ) + + assert resolved.aws_region_name == "us-east-1" + assert resolved.presidio_analyzer_api_base == "https://analyzer.example" + assert resolved.api_key == "sk-guardrail" + # An unset name must not become the string "None", which boto3 would treat as a credential. + assert resolved.aws_secret_access_key is None + # Literal values are left alone, and unrelated fields survive the copy. + assert resolved.aws_access_key_id == "AKIA_LITERAL_VALUE" + assert resolved.guardrail == "bedrock" + assert resolved.mode == "pre_call"