diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 0a5364bfcfec..518a28f11458 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -3015,7 +3015,8 @@ def create_file( # Store the upload URL in litellm_params for the transformation method litellm_params_with_url = dict(litellm_params) - litellm_params_with_url["upload_url"] = api_base + if "upload_url" not in litellm_params: + litellm_params_with_url["upload_url"] = api_base return provider_config.transform_create_file_response( model=None, diff --git a/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py b/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py index 37a0daa1d500..fb09e5b6194b 100644 --- a/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py +++ b/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py @@ -5,9 +5,11 @@ import base64 from unittest.mock import AsyncMock, MagicMock, patch +import httpx import pytest import litellm +from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.types.llms.openai import HttpxBinaryResponseContent from litellm.types.utils import SpecialEnums @@ -108,3 +110,64 @@ async def test_litellm_afile_content_bedrock_provider_with_unified_file_id(self) assert call_kwargs["_is_async"] is True # The handler extracts S3 URI from the unified file ID assert call_kwargs["file_content_request"]["file_id"] == encoded_file_id + + def test_sync_create_file_preserves_provider_upload_url(self): + """ + Test that sync create_file does not overwrite upload_url set by the + provider's transform_create_file_request. + + Regression test for https://github.com/BerriAI/litellm/issues/21546 + where the sync path unconditionally overwrote litellm_params["upload_url"] + with api_base, causing the returned file_id to point to the wrong S3 key. + """ + handler = BaseLLMHTTPHandler() + + # The provider sets upload_url during transform_create_file_request + # (this is what Bedrock does at bedrock/files/transformation.py:370) + provider_upload_url = "https://s3.us-east-1.amazonaws.com/bucket/correct-uuid-key.jsonl" + # api_base holds a stale URL from the first get_complete_file_url call + stale_api_base = "https://s3.us-east-1.amazonaws.com/bucket/stale-uuid-key.jsonl" + + litellm_params = {"upload_url": provider_upload_url} + + mock_provider = MagicMock() + # transform_create_file_request returns a pre-signed dict (Bedrock path) + mock_provider.transform_create_file_request.return_value = { + "method": "PUT", + "url": provider_upload_url, + "headers": {"Authorization": "AWS4-HMAC-SHA256 ..."}, + "data": b"file-content", + } + mock_provider.validate_environment.return_value = {} + mock_provider.get_complete_file_url.return_value = stale_api_base + + mock_response = httpx.Response( + status_code=200, + headers={"ETag": '"abc123"', "Content-Length": "100"}, + request=httpx.Request(method="PUT", url=provider_upload_url), + ) + mock_provider.transform_create_file_response.return_value = MagicMock() + + mock_client = MagicMock() + mock_client.put.return_value = mock_response + + handler.create_file( + create_file_data={"file": b"data", "purpose": "batch"}, + litellm_params=litellm_params, + provider_config=mock_provider, + api_base=stale_api_base, + api_key="fake-key", + headers={}, + logging_obj=MagicMock(), + client=mock_client, + timeout=30, + _is_async=False, + ) + + # The key assertion: transform_create_file_response must receive + # the provider's upload_url, NOT the stale api_base + call_kwargs = mock_provider.transform_create_file_response.call_args.kwargs + assert call_kwargs["litellm_params"]["upload_url"] == provider_upload_url, ( + "upload_url was overwritten with stale api_base; " + "provider's upload_url should be preserved" + )