From 76867f3aa3e6265712e7dbe9ea57510b260769cf Mon Sep 17 00:00:00 2001 From: samarth9008 Date: Mon, 6 Apr 2026 19:57:31 -0400 Subject: [PATCH] fix --- litellm/proxy/litellm_pre_call_utils.py | 11 +++- .../proxy/test_litellm_pre_call_utils.py | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 3ed96c163af..a92455d5839 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -667,7 +667,9 @@ class LiteLLMProxyRequestSetup: ) if isinstance(data[_metadata_variable_name], dict): - data[_metadata_variable_name].update(metadata_from_headers) + for key, value in metadata_from_headers.items(): + if key not in data[_metadata_variable_name]: + data[_metadata_variable_name][key] = value return data @staticmethod @@ -1063,8 +1065,11 @@ async def add_litellm_data_to_request( # noqa: PLR0915 ) else: data["litellm_metadata"] = parsed_litellm_metadata - # Merge litellm_metadata into the metadata variable (preserving existing values) - if isinstance(data["litellm_metadata"], dict): + # Merge litellm_metadata into the metadata variable (preserving existing values). + # Skip when both sides are the same dict (LITELLM_METADATA_ROUTES like /v1/messages + # where _metadata_variable_name == "litellm_metadata") — merging a dict with itself + # is a no-op and would mask the self-reference bug. + if isinstance(data["litellm_metadata"], dict) and data["litellm_metadata"] is not data[_metadata_variable_name]: for key, value in data["litellm_metadata"].items(): if key not in data[_metadata_variable_name]: data[_metadata_variable_name][key] = value diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 04af5cd0086..314227d1765 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -10,6 +10,8 @@ from fastapi import Request import litellm from litellm.proxy._types import TeamCallbackMetadata, UserAPIKeyAuth +from unittest.mock import MagicMock +from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup from litellm.proxy.litellm_pre_call_utils import ( KeyAndTeamLoggingSettings, LiteLLMProxyRequestSetup, @@ -1912,3 +1914,61 @@ async def test_bearer_token_not_in_debug_logs(): f"Bearer token leaked in debug logs. " f"Found token in log output:\n{log_output[:500]}" ) + + +def test_litellm_metadata_not_overridden_by_headers_on_messages_endpoint(): + """ + Regression test for https://github.com/BerriAI/litellm/issues/24945 + + User-supplied values in litellm_metadata (e.g. trace_id, tags) should NOT be + overwritten by proxy-injected header values on /v1/messages (and other + LITELLM_METADATA_ROUTES). Body values take priority over header values. + """ + # Simulate /v1/messages — _metadata_variable_name == "litellm_metadata" + _metadata_variable_name = "litellm_metadata" + # User supplied trace_id and tags in the request body + data: dict = { + _metadata_variable_name: { + "trace_id": "from-body", + "tags": ["user-tag"], + } + } + # Proxy receives x-litellm-trace-id and x-litellm-tags via headers + headers = { + "x-litellm-trace-id": "from-header", + "x-litellm-tags": '["proxy-tag"]', + } + LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( + headers=headers, + data=data, + _metadata_variable_name=_metadata_variable_name, + ) + # Body values must NOT be overwritten by header values + assert data[_metadata_variable_name]["trace_id"] == "from-body", ( + "trace_id from request body was overridden by x-litellm-trace-id header" + ) + assert data[_metadata_variable_name]["tags"] == ["user-tag"], ( + "tags from request body were overridden by x-litellm-tags header" + ) + + +def test_header_metadata_added_when_not_in_body_on_messages_endpoint(): + """ + Companion to the regression test above: header-supplied metadata fields that + the user did NOT provide in the body should still be injected by the proxy. + """ + _metadata_variable_name = "litellm_metadata" + # User sends no trace_id in the body + data: dict = {_metadata_variable_name: {}} + headers = { + "x-litellm-trace-id": "from-header", + } + LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( + headers=headers, + data=data, + _metadata_variable_name=_metadata_variable_name, + ) + # Header value should be set since user did not supply it + assert data[_metadata_variable_name]["trace_id"] == "from-header", ( + "trace_id from header was not injected when body did not supply it" + )