From 899ed6786047ffa4dbf9e6e138777debf9032743 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Thu, 30 Jul 2026 16:42:23 -0700 Subject: [PATCH] fix(logging): bind litellm_metadata by reference in function_setup so guardrail info reaches spend logs --- litellm/utils.py | 2 +- .../test_litellm_logging.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/litellm/utils.py b/litellm/utils.py index 944bb61d5e7..db78cc0af7f 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1048,7 +1048,7 @@ def function_setup( if "metadata" in kwargs: litellm_params["metadata"] = kwargs["metadata"] if "litellm_metadata" in kwargs and isinstance(kwargs["litellm_metadata"], dict): - litellm_params["litellm_metadata"] = kwargs["litellm_metadata"].copy() + litellm_params["litellm_metadata"] = kwargs["litellm_metadata"] # For endpoints like /v1/messages that use "litellm_metadata" instead # of "metadata" (to avoid conflicting with provider API metadata fields), # populate litellm_params["metadata"] so callbacks (e.g. Langfuse) that diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index edc257f4c3f..eaa4bd3e3fc 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2992,6 +2992,58 @@ def test_function_setup_litellm_metadata_populates_metadata(): ), "litellm_params['metadata'] should be a copy, not the same object" +def test_function_setup_litellm_metadata_guardrail_writes_visible_after_setup(): + """ + Regression test for LIT-4512: guardrail writes into the request's + "litellm_metadata" bucket that happen AFTER function_setup (the proxy + initializes the logging object before pre-call guardrails run) must be + visible to the logging object and survive merge_litellm_metadata, so + /v1/messages spend logs carry guardrail_information and + applied_guardrails just like /v1/chat/completions. + """ + import litellm + from litellm.litellm_core_utils.core_helpers import get_or_create_metadata_bucket + from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup + + kwargs = { + "model": "claude-3-5-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "test-call-id-lit4512", + "litellm_metadata": { + "user_api_key_hash": "sk-hashed-lit4512", + "guardrails": ["pam-ethical-request"], + }, + } + + logging_obj, returned_kwargs = litellm.utils.function_setup( + original_function="anthropic_messages", + rules_obj=litellm.utils.Rules(), + start_time=time.time(), + **kwargs, + ) + + guardrail_entry = { + "guardrail_name": "pam-ethical-request", + "guardrail_mode": "pre_call", + "guardrail_status": "success", + } + _, metadata_bucket = get_or_create_metadata_bucket(returned_kwargs) + metadata_bucket["standard_logging_guardrail_information"] = [guardrail_entry] + metadata_bucket["applied_guardrails"] = ["pam-ethical-request"] + + litellm_params = logging_obj.model_call_details.get("litellm_params", {}) + litellm_metadata = litellm_params.get("litellm_metadata") + assert litellm_metadata is not None + assert litellm_metadata.get("standard_logging_guardrail_information") == [ + guardrail_entry + ], "guardrail writes after function_setup must be visible to the logging object" + assert litellm_metadata.get("applied_guardrails") == ["pam-ethical-request"] + + merged = StandardLoggingPayloadSetup.merge_litellm_metadata(litellm_params) + assert merged.get("standard_logging_guardrail_information") == [guardrail_entry] + assert merged.get("applied_guardrails") == ["pam-ethical-request"] + + def test_function_setup_metadata_takes_precedence_over_litellm_metadata(): """ Test that when BOTH metadata and litellm_metadata are present (e.g., user sets