From 3fa2bd34bb0a044a1b0dfb45913f85cf0837f78f Mon Sep 17 00:00:00 2001 From: Varshith Date: Fri, 22 May 2026 09:52:03 -0500 Subject: [PATCH] fix(fallbacks): ignore preserve_litellm_internal_headers for raw httpx.Headers inputs --- litellm/litellm_core_utils/core_helpers.py | 24 +++++++++++++------ .../litellm_core_utils/test_fallback_utils.py | 20 ++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 536ac309f42..8b96d8355de 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -247,15 +247,25 @@ def process_response_headers( preserve_litellm_internal_headers: bool = False, ) -> dict: """ - `preserve_litellm_internal_headers` must only be True when the input dict is - a LiteLLM-owned structure (e.g. `_hidden_params["additional_headers"]` that - has already been through one round of processing). For raw upstream - provider headers it must remain False, otherwise a malicious provider - returning `x-litellm-*` could spoof LiteLLM-internal markers - (e.g. `x-litellm-attempted-fallbacks`). + `preserve_litellm_internal_headers` must only be True when the input is a + LiteLLM-owned dict (e.g. `_hidden_params["additional_headers"]` that has + already been through one round of processing). For raw upstream provider + headers — whether passed as `httpx.Headers` or a plain dict — it must + remain False, otherwise a malicious provider returning `x-litellm-*` could + spoof LiteLLM-internal markers (e.g. `x-litellm-attempted-fallbacks`). + + When the input is an `httpx.Headers` object the flag is always treated as + False regardless of what the caller requested, because `httpx.Headers` is + always a raw provider response and can never be LiteLLM-owned. """ from litellm.types.utils import OPENAI_RESPONSE_HEADERS + # Raw httpx.Headers objects come directly from provider HTTP responses and + # must never be treated as LiteLLM-owned, regardless of caller intent. + _preserve = preserve_litellm_internal_headers and isinstance( + response_headers, dict + ) + openai_headers = {} processed_headers = {} additional_headers = {} @@ -267,7 +277,7 @@ def process_response_headers( "llm_provider-" ): # return raw provider headers (incl. openai-compatible ones) processed_headers[k] = v - elif preserve_litellm_internal_headers and k.startswith("x-litellm-"): + elif _preserve and k.startswith("x-litellm-"): # LiteLLM's own internal headers (e.g. x-litellm-attempted-fallbacks, # x-litellm-model-group) are not LLM provider headers and must not be # prefixed. Downstream consumers (proxy override, callers checking diff --git a/tests/test_litellm/litellm_core_utils/test_fallback_utils.py b/tests/test_litellm/litellm_core_utils/test_fallback_utils.py index 142602ccb27..90a61696e9d 100644 --- a/tests/test_litellm/litellm_core_utils/test_fallback_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_fallback_utils.py @@ -1,6 +1,7 @@ """Tests for litellm.litellm_core_utils.fallback_utils.""" import pytest +import httpx import litellm from litellm.litellm_core_utils.core_helpers import process_response_headers @@ -147,3 +148,22 @@ def test_process_response_headers_prefixes_x_litellm_from_raw_provider(): assert "x-litellm-attempted-fallbacks" not in result assert result["llm_provider-x-litellm-attempted-fallbacks"] == 99 assert result["llm_provider-x-stainless-arch"] == "arm64" + + +def test_process_response_headers_ignores_preserve_flag_for_httpx_headers(): + """ + Some providers store raw httpx.Headers directly in _hidden_params["additional_headers"] + without a prior normalization pass. If preserve_litellm_internal_headers=True were + honored for httpx.Headers inputs, a provider returning x-litellm-attempted-fallbacks + could spoof it as a bare LiteLLM-internal marker and make the proxy skip + stamping the correct response model. The flag must be ignored for httpx.Headers. + """ + raw = httpx.Headers( + { + "x-litellm-attempted-fallbacks": "1", + "content-type": "application/json", + } + ) + result = process_response_headers(raw, preserve_litellm_internal_headers=True) + assert "x-litellm-attempted-fallbacks" not in result + assert result["llm_provider-x-litellm-attempted-fallbacks"] == "1"