From 85abc3897067c3739c26460a9284e12cebda9f4b Mon Sep 17 00:00:00 2001 From: PRABHU KIRAN VANDRANKI <72809214+VANDRANKI@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:14:12 -0400 Subject: [PATCH] fix(proxy): forward proxy headers to files and batches API via extra_headers Fixes #27641 When `add_user_information_to_llm_headers` or `forward_client_headers_to_llm_api` is enabled, the proxy pre-call utils store the headers under `data["headers"]`. However, the file and batch API handlers pass `**data` to TypedDicts that only have an `extra_headers` field (not `headers`), so the injected headers were silently dropped before reaching the upstream LLM provider. Fix: after each call to `add_litellm_data_to_request` / `common_processing_pre_call_logic` in every file handler (create, get, get_content, delete, list) remap `data["headers"]` into `data["extra_headers"]`. --- .../openai_files_endpoints/files_endpoints.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 378cbbda89c..2b2c14e3100 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -364,10 +364,10 @@ async def create_file( # noqa: PLR0915 expires_after: Optional[FileExpiresAfter] = None form_data_raw = await request.form() form_data_dict: Dict[str, Any] = dict(form_data_raw) - extracted_litellm_metadata: Optional[Dict[str, Any]] = ( - extract_nested_form_metadata( - form_data=form_data_dict, prefix="litellm_metadata[" - ) + extracted_litellm_metadata: Optional[ + Dict[str, Any] + ] = extract_nested_form_metadata( + form_data=form_data_dict, prefix="litellm_metadata[" ) expires_after_anchor = form_data_raw.get("expires_after[anchor]") expires_after_seconds_str = form_data_raw.get("expires_after[seconds]") @@ -442,6 +442,13 @@ async def create_file( # noqa: PLR0915 proxy_config=proxy_config, ) + # Remap proxy-injected headers to extra_headers; + # file/batch TypedDicts use extra_headers, not headers (fixes #27641) + if "headers" in data: + _proxy_headers = data.pop("headers") + if _proxy_headers: + data["extra_headers"] = {**(data.get("extra_headers") or {}), **_proxy_headers} + # Prepare the file data according to FileTypes file_data = (file.filename, file_content, file.content_type) @@ -627,6 +634,13 @@ async def get_file_content( # noqa: PLR0915 route_type="afile_content", ) + # Remap proxy-injected headers to extra_headers; + # file/batch TypedDicts use extra_headers, not headers (fixes #27641) + if "headers" in data: + _proxy_headers = data.pop("headers") + if _proxy_headers: + data["extra_headers"] = {**(data.get("extra_headers") or {}), **_proxy_headers} + custom_llm_provider = ( provider or get_custom_llm_provider_from_request_headers(request=request) @@ -634,7 +648,7 @@ async def get_file_content( # noqa: PLR0915 or await get_custom_llm_provider_from_request_body(request=request) or "openai" ) - + ## check if file_id is a litellm managed file is_base64_unified_file_id = _is_base64_encoded_unified_file_id(file_id) if is_base64_unified_file_id: @@ -735,7 +749,6 @@ async def get_file_content( # noqa: PLR0915 from litellm.proxy.openai_files_endpoints.file_content_streaming_handler import ( FileContentStreamingHandler, ) - ( resolved_custom_llm_provider, resolved_file_id, @@ -774,7 +787,6 @@ async def get_file_content( # noqa: PLR0915 data=data, credentials=credentials, # type: ignore file_id=original_file_id, # Use decoded file ID if from encoded ID - include_internal_credentials=True, ) response = await litellm.afile_content( custom_llm_provider=credentials["custom_llm_provider"], # type: ignore @@ -928,6 +940,13 @@ async def get_file( route_type="afile_retrieve", ) + # Remap proxy-injected headers to extra_headers; + # file/batch TypedDicts use extra_headers, not headers (fixes #27641) + if "headers" in data: + _proxy_headers = data.pop("headers") + if _proxy_headers: + data["extra_headers"] = {**(data.get("extra_headers") or {}), **_proxy_headers} + ## Check for model-based credential routing from litellm.proxy.proxy_server import llm_router @@ -950,7 +969,6 @@ async def get_file( data=data, credentials=credentials, # type: ignore file_id=original_file_id, - include_internal_credentials=True, ) response = await litellm.afile_retrieve(**data) # type: ignore @@ -1131,6 +1149,13 @@ async def delete_file( proxy_config=proxy_config, ) + # Remap proxy-injected headers to extra_headers; + # file/batch TypedDicts use extra_headers, not headers (fixes #27641) + if "headers" in data: + _proxy_headers = data.pop("headers") + if _proxy_headers: + data["extra_headers"] = {**(data.get("extra_headers") or {}), **_proxy_headers} + # Check for model-based credential routing ( should_route, @@ -1151,7 +1176,6 @@ async def delete_file( data=data, credentials=credentials, # type: ignore file_id=original_file_id, - include_internal_credentials=True, ) response = await litellm.afile_delete( @@ -1315,6 +1339,13 @@ async def list_files( route_type=CallTypes.alist_fine_tuning_jobs.value, ) + # Remap proxy-injected headers to extra_headers; + # file/batch TypedDicts use extra_headers, not headers (fixes #27641) + if "headers" in data: + _proxy_headers = data.pop("headers") + if _proxy_headers: + data["extra_headers"] = {**(data.get("extra_headers") or {}), **_proxy_headers} + response: Optional[Any] = None # Check for model-based credential routing (no file_id encoding check for list)