From 5994ef90944e7fae543c3ff011ac073c14e9cc28 Mon Sep 17 00:00:00 2001 From: shivam Date: Sat, 23 May 2026 14:22:28 -0700 Subject: [PATCH] fix(proxy): merge metadata buckets for batch policy response headers Ensure get_logging_caching_headers reads both metadata and litellm_metadata so policy/guardrail headers are emitted on batch routes with user metadata, and log dropped non-string OpenAI metadata at debug level. Co-authored-by: Cursor --- litellm/proxy/common_utils/callback_utils.py | 20 ++++++++++++++----- .../proxy/common_utils/test_callback_utils.py | 18 +++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index 7d558781320..c5b97db07a0 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -409,11 +409,15 @@ def get_remaining_tokens_and_requests_from_request_data(data: Dict) -> Dict[str, def get_logging_caching_headers(request_data: Dict) -> Optional[Dict]: - _metadata = request_data.get("metadata", None) - if not _metadata: - _metadata = request_data.get("litellm_metadata", None) - if not isinstance(_metadata, dict): - _metadata = {} + _metadata: Dict = {} + metadata_bucket = request_data.get("metadata") + litellm_metadata_bucket = request_data.get("litellm_metadata") + if isinstance(metadata_bucket, dict): + _metadata.update(metadata_bucket) + if isinstance(litellm_metadata_bucket, dict): + # Batch/file routes store proxy tracking in litellm_metadata while + # user-facing metadata stays in metadata; merge both for headers. + _metadata.update(litellm_metadata_bucket) headers = {} if "applied_guardrails" in _metadata: headers["x-litellm-applied-guardrails"] = ",".join( @@ -529,6 +533,12 @@ def sanitize_openai_provider_metadata( continue if isinstance(value, str): sanitized[key] = value + else: + verbose_proxy_logger.debug( + "sanitize_openai_provider_metadata: dropping key %r with non-string value of type %s", + key, + type(value).__name__, + ) return sanitized or None diff --git a/tests/test_litellm/proxy/common_utils/test_callback_utils.py b/tests/test_litellm/proxy/common_utils/test_callback_utils.py index 9ece72c2f99..d328d68dcd4 100644 --- a/tests/test_litellm/proxy/common_utils/test_callback_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_callback_utils.py @@ -11,6 +11,7 @@ from litellm.proxy.common_utils.callback_utils import ( add_policy_to_applied_policies_header, decrypt_callback_vars, encrypt_callback_vars, + get_logging_caching_headers, initialize_callbacks_on_proxy, get_remaining_tokens_and_requests_from_request_data, normalize_callback_names, @@ -121,6 +122,23 @@ def test_sanitize_openai_provider_metadata_strips_internal_tracking_fields(): assert sanitized == {"customer_id": "cust-123"} +def test_get_logging_caching_headers_merges_metadata_and_litellm_metadata(): + request_data = { + "metadata": {"customer_id": "cust-123"}, + "litellm_metadata": { + "applied_policies": ["global-baseline"], + "applied_guardrails": ["pii_blocker"], + "policy_sources": {"global-baseline": "team_default"}, + }, + } + + headers = get_logging_caching_headers(request_data) + + assert headers["x-litellm-applied-policies"] == "global-baseline" + assert headers["x-litellm-applied-guardrails"] == "pii_blocker" + assert headers["x-litellm-policy-sources"] == "global-baseline=team_default" + + def test_initialize_callbacks_on_proxy_instantiates_compression_interception( monkeypatch, ):