diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index d3ca2fa64ed..be29b4ec5b6 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -88,7 +88,10 @@ from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.prometheus import PrometheusLogger from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting from litellm.integrations.SlackAlerting.utils import _add_langfuse_trace_id_to_alert -from litellm.litellm_core_utils.core_helpers import coerce_token_limit +from litellm.litellm_core_utils.core_helpers import ( + coerce_token_limit, + get_metadata_variable_name_from_kwargs, +) from litellm.litellm_core_utils.litellm_logging import Logging from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_loads import safe_json_loads @@ -1248,7 +1251,7 @@ class ProxyLogging: Returns the (possibly modified) data dict. """ - metadata: Final = data.get("metadata", data.get("litellm_metadata", {})) or {} + metadata: Final = data.get(get_metadata_variable_name_from_kwargs(data)) or {} pipelines: Final = metadata.get("_guardrail_pipelines") if not pipelines: return data @@ -1400,7 +1403,7 @@ class ProxyLogging: ) # Get pipeline-managed guardrails to skip in normal loop - metadata: Final = data.get("metadata", data.get("litellm_metadata", {})) or {} + metadata: Final = data.get(get_metadata_variable_name_from_kwargs(data)) or {} pipeline_managed: Final[set] = metadata.get("_pipeline_managed_guardrails", set()) caps: Final = ProxyLogging._callback_capabilities() diff --git a/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py b/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py index 5c711fc6c34..98eeec21dab 100644 --- a/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py +++ b/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py @@ -350,6 +350,44 @@ async def test_maybe_execute_pipelines_skips_pipelines_with_other_mode(proxy_log assert out is data +@pytest.mark.asyncio +async def test_maybe_execute_pipelines_reads_litellm_metadata_when_caller_sends_own_metadata( + proxy_logging, make_user_api_key_auth, monkeypatch +): + """On /v1/messages the proxy stores policy state in ``litellm_metadata``, while the + caller's provider-facing ``metadata`` (Claude Code sends ``metadata.user_id``) stays + untouched. The pipeline must still run.""" + pipeline = MagicMock() + pipeline.mode = "pre_call" + pipeline.steps = [] + fake_result = MagicMock() + fake_result.terminal_action = "allow" + fake_result.modified_data = None + fake_result.step_results = [] + data = { + "metadata": {"user_id": "user_abc"}, + "litellm_metadata": {"_guardrail_pipelines": [("policy-1", pipeline)]}, + "messages": [], + "model": "m", + } + executed = MagicMock(return_value=fake_result) + + async def fake_execute_steps(**kwargs): + return executed(**kwargs) + + monkeypatch.setattr( + "litellm.proxy.policy_engine.pipeline_executor.PipelineExecutor.execute_steps", + fake_execute_steps, + ) + await proxy_logging._maybe_execute_pipelines( + data=data, + user_api_key_dict=make_user_api_key_auth(), + call_type="anthropic_messages", + event_hook="pre_call", + ) + executed.assert_called_once() + + @pytest.mark.asyncio async def test_maybe_execute_pipelines_blocks_on_block_terminal_action_raises( proxy_logging, make_user_api_key_auth, monkeypatch