From 4d5924dbafe69769c05fa9b600e3dc9d17fd83d1 Mon Sep 17 00:00:00 2001 From: ege-arhan Date: Thu, 22 Oct 2026 23:40:46 +0000 Subject: [PATCH] fix(proxy): case-insensitive User-Agent header lookup in _get_user_agent_tags Header names are not case-sensitive, so a client sending "User-Agent" was untagged while "user-agent" was tagged. Look the header up case-insensitively and early-return on non-dict headers instead of nesting the whole body. Test moved to tests/unit/litellm_core_utils/ after #43199. --- litellm/litellm_core_utils/litellm_logging.py | 32 ++++++++++--------- .../test_litellm_logging.py | 18 +++++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index fc8d1ecc8c1..53c980a3012 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -7062,21 +7062,23 @@ class StandardLoggingPayloadSetup: return None user_agent_tags: list[str] | None = None headers: Final = proxy_server_request.get("headers", {}) - if headers is not None and isinstance(headers, dict): - request_headers: Final = cast( # cast-ok: request headers are untyped framework data - dict[str, str | None], headers - ) - if "user-agent" in request_headers: - user_agent: Final = request_headers["user-agent"] - if user_agent is not None: - if user_agent_tags is None: - user_agent_tags = [] - user_agent_part: str | None = None - if "/" in user_agent: - user_agent_part = user_agent.split("/")[0] - if user_agent_part is not None: - user_agent_tags.append("User-Agent: " + user_agent_part) - user_agent_tags.append("User-Agent: " + user_agent) + if not isinstance(headers, dict): + return None + # Header names are case-insensitive: a client that sends "User-Agent" + # or "USER-AGENT" must still be tagged (#40979). + user_agent: Final = next( + (v for k, v in headers.items() if str(k).lower() == "user-agent"), + None, + ) + if user_agent is not None: + if user_agent_tags is None: + user_agent_tags = [] + user_agent_part: str | None = None + if "/" in user_agent: + user_agent_part = user_agent.split("/")[0] + if user_agent_part is not None: + user_agent_tags.append("User-Agent: " + user_agent_part) + user_agent_tags.append("User-Agent: " + user_agent) return user_agent_tags @staticmethod diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index 8509b1d6f7d..1618d8b9dbc 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -3386,6 +3386,24 @@ def test_get_user_agent_tags(): assert "User-Agent: litellm" in tags assert "User-Agent: litellm/0.1.0" in tags + # Header names are case-insensitive ("User-Agent" vs "user-agent"). + tags_capitalized = StandardLoggingPayloadSetup._get_user_agent_tags( + proxy_server_request={ + "headers": { + "User-Agent": "curl/7.68.0", + } + } + ) + + assert tags_capitalized is not None + assert "User-Agent: curl" in tags_capitalized + assert "User-Agent: curl/7.68.0" in tags_capitalized + + # Non-dict headers must not raise; the function early-returns. + assert StandardLoggingPayloadSetup._get_user_agent_tags( + proxy_server_request={"headers": None} + ) is None + def test_get_request_tags(): from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup