diff --git a/litellm/llms/base_llm/files/transformation.py b/litellm/llms/base_llm/files/transformation.py
index ec0b28a63ad..cc36962da6b 100644
--- a/litellm/llms/base_llm/files/transformation.py
+++ b/litellm/llms/base_llm/files/transformation.py
@@ -135,6 +135,9 @@ class BaseFilesConfig(BaseConfig):
) -> OpenAIFileObject:
"""Transform file retrieve response into OpenAI format."""
+ def is_retrieve_file_response_successful(self, response: httpx.Response) -> bool:
+ return not httpx.codes.is_error(response.status_code)
+
@abstractmethod
def transform_delete_file_request(
self,
diff --git a/litellm/llms/bedrock/files/transformation.py b/litellm/llms/bedrock/files/transformation.py
index c4aa84004dc..fe3d69303d1 100644
--- a/litellm/llms/bedrock/files/transformation.py
+++ b/litellm/llms/bedrock/files/transformation.py
@@ -79,8 +79,22 @@ LIST_FILES_PURPOSE_PARAM: Final = "_s3_list_files_purpose"
LIST_FILES_LOCATION_PARAM: Final = "_s3_list_files_location"
+def _is_empty_s3_object_range_error(raw_response: Response) -> bool:
+ if raw_response.status_code != 416:
+ return False
+ if raw_response.headers.get("Content-Range") == "bytes */0":
+ return True
+ try:
+ error_xml: Final = ET.fromstring(raw_response.content)
+ except ET.ParseError:
+ return False
+ return error_xml.findtext("ActualObjectSize") == "0"
+
+
def _retrieved_s3_file_size(raw_response: Response) -> int:
status_code: Final = raw_response.status_code
+ if _is_empty_s3_object_range_error(raw_response):
+ return 0
if status_code == 206:
content_range: Final = raw_response.headers.get("Content-Range", "")
range_parts: Final = content_range.removeprefix("bytes 0-0/")
@@ -439,6 +453,9 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
def custom_llm_provider(self) -> LlmProviders:
return LlmProviders.BEDROCK
+ def is_retrieve_file_response_successful(self, response: httpx.Response) -> bool:
+ return not httpx.codes.is_error(response.status_code) or _is_empty_s3_object_range_error(response)
+
@property
def file_upload_http_method(self) -> str:
"""
diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py
index b57ce8ea2e8..f09e6c2bfe0 100644
--- a/litellm/llms/custom_httpx/llm_http_handler.py
+++ b/litellm/llms/custom_httpx/llm_http_handler.py
@@ -4660,7 +4660,8 @@ class BaseLLMHTTPHandler:
except Exception as e:
raise self._handle_error(e=e, provider_config=provider_config)
- self._raise_for_provider_error_status(response=response, provider_config=provider_config)
+ if not provider_config.is_retrieve_file_response_successful(response):
+ self._raise_for_provider_error_status(response=response, provider_config=provider_config)
return provider_config.transform_retrieve_file_response(
raw_response=response,
logging_obj=logging_obj,
@@ -4718,7 +4719,8 @@ class BaseLLMHTTPHandler:
except Exception as e:
raise self._handle_error(e=e, provider_config=provider_config)
- self._raise_for_provider_error_status(response=response, provider_config=provider_config)
+ if not provider_config.is_retrieve_file_response_successful(response):
+ self._raise_for_provider_error_status(response=response, provider_config=provider_config)
return provider_config.transform_retrieve_file_response(
raw_response=response,
logging_obj=logging_obj,
diff --git a/tests/unit/llms/bedrock/files/test_bedrock_files_transformation.py b/tests/unit/llms/bedrock/files/test_bedrock_files_transformation.py
index a647b94b6e8..1a919e7b6ad 100644
--- a/tests/unit/llms/bedrock/files/test_bedrock_files_transformation.py
+++ b/tests/unit/llms/bedrock/files/test_bedrock_files_transformation.py
@@ -2276,7 +2276,33 @@ class TestBedrockFileContentTransformation:
assert response.status == "processed"
assert response.object == "file"
- def test_transform_retrieve_file_response_raises_for_empty_object(self, monkeypatch):
+ def test_transform_retrieve_file_response_accepts_verified_empty_object(self, monkeypatch):
+ import httpx
+
+ from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
+
+ monkeypatch.setenv("AWS_S3_BUCKET_NAME", "my-bucket")
+ litellm_params = self._litellm_params()
+ BedrockFilesConfig().transform_retrieve_file_request(
+ file_id=self.S3_URI,
+ optional_params={},
+ litellm_params=litellm_params,
+ )
+ response = BedrockFilesConfig().transform_retrieve_file_response(
+ raw_response=httpx.Response(
+ 416,
+ content=b"InvalidRange0",
+ request=httpx.Request("GET", self.S3_URI),
+ ),
+ logging_obj=MagicMock(),
+ litellm_params=litellm_params,
+ )
+
+ assert response.bytes == 0
+ assert response.filename == "input.jsonl.out"
+ assert response.purpose == "batch_output"
+
+ def test_transform_retrieve_file_response_rejects_unverified_empty_object(self, monkeypatch):
import httpx
from litellm.llms.bedrock.common_utils import BedrockError
@@ -2293,7 +2319,7 @@ class TestBedrockFileContentTransformation:
BedrockFilesConfig().transform_retrieve_file_response(
raw_response=httpx.Response(
416,
- headers={"Content-Range": "bytes */0"},
+ content=b"InvalidRange",
request=httpx.Request("GET", self.S3_URI),
),
logging_obj=MagicMock(),
diff --git a/tests/unit/llms/custom_httpx/test_llm_http_handler.py b/tests/unit/llms/custom_httpx/test_llm_http_handler.py
index 82cc9e9d3ac..25d2d11b5ca 100644
--- a/tests/unit/llms/custom_httpx/test_llm_http_handler.py
+++ b/tests/unit/llms/custom_httpx/test_llm_http_handler.py
@@ -45,6 +45,7 @@ from litellm.llms.azure.videos.transformation import AzureVideoConfig
from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import (
AmazonAnthropicClaudeMessagesConfig,
)
+from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
from litellm.llms.anthropic.skills.transformation import AnthropicSkillsConfig
from litellm.llms.openai.evals.transformation import OpenAIEvalsConfig
from litellm.llms.mistral.files.transformation import MistralFilesConfig
@@ -4925,3 +4926,133 @@ async def test_lookup_handlers_raise_the_provider_error_status(name: str, is_asy
assert error.value.status_code == status_code
assert "No such object" in error.value.message
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("is_async", (False, True))
+@pytest.mark.parametrize(
+ ("status_code", "content", "headers", "expected_bytes"),
+ (
+ (
+ 416,
+ b"InvalidRange0",
+ {},
+ 0,
+ ),
+ (206, b"", {"Content-Range": "bytes 0-0/4321"}, 4321),
+ ),
+)
+async def test_retrieve_file_accepts_bedrock_successful_range_responses(
+ is_async: bool,
+ status_code: int,
+ content: bytes,
+ headers: dict[str, str],
+ expected_bytes: int,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setenv("AWS_S3_BUCKET_NAME", "my-bucket")
+ file_id = "s3://my-bucket/litellm-batch-outputs/job-123/output.jsonl"
+ transport = httpx.MockTransport(
+ lambda request: httpx.Response(
+ status_code,
+ content=content,
+ headers=headers,
+ request=request,
+ )
+ )
+ params = {
+ "aws_access_key_id": "AKIAEXAMPLE",
+ "aws_secret_access_key": "secret",
+ "aws_region_name": "us-west-2",
+ }
+ handler = BaseLLMHTTPHandler()
+
+ if is_async:
+ client = AsyncHTTPHandler()
+ await client.close()
+ async_client = httpx.AsyncClient(transport=transport)
+ client.client = async_client
+ try:
+ result = await handler.async_retrieve_file(
+ file_id=file_id,
+ provider_config=BedrockFilesConfig(),
+ litellm_params=params,
+ headers={},
+ logging_obj=Mock(),
+ client=client,
+ )
+ finally:
+ await async_client.aclose()
+ else:
+ sync_client = httpx.Client(transport=transport)
+ client = HTTPHandler(client=sync_client)
+ try:
+ result = handler.retrieve_file(
+ file_id=file_id,
+ provider_config=BedrockFilesConfig(),
+ litellm_params=params,
+ headers={},
+ logging_obj=Mock(),
+ client=client,
+ )
+ finally:
+ sync_client.close()
+
+ assert result.bytes == expected_bytes
+ assert result.filename == "output.jsonl"
+ assert result.purpose == "batch_output"
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("is_async", (False, True))
+async def test_retrieve_file_rejects_unverified_bedrock_range_error(
+ is_async: bool, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setenv("AWS_S3_BUCKET_NAME", "my-bucket")
+ file_id = "s3://my-bucket/litellm-batch-outputs/job-123/output.jsonl"
+ transport = httpx.MockTransport(
+ lambda request: httpx.Response(
+ 416,
+ content=b"InvalidRange",
+ request=request,
+ )
+ )
+ params = {
+ "aws_access_key_id": "AKIAEXAMPLE",
+ "aws_secret_access_key": "secret",
+ "aws_region_name": "us-west-2",
+ }
+ handler = BaseLLMHTTPHandler()
+
+ if is_async:
+ client = AsyncHTTPHandler()
+ await client.close()
+ async_client = httpx.AsyncClient(transport=transport)
+ client.client = async_client
+ try:
+ with pytest.raises(BaseLLMException, match="InvalidRange"):
+ await handler.async_retrieve_file(
+ file_id=file_id,
+ provider_config=BedrockFilesConfig(),
+ litellm_params=params,
+ headers={},
+ logging_obj=Mock(),
+ client=client,
+ )
+ finally:
+ await async_client.aclose()
+ else:
+ sync_client = httpx.Client(transport=transport)
+ client = HTTPHandler(client=sync_client)
+ try:
+ with pytest.raises(BaseLLMException, match="InvalidRange"):
+ handler.retrieve_file(
+ file_id=file_id,
+ provider_config=BedrockFilesConfig(),
+ litellm_params=params,
+ headers={},
+ logging_obj=Mock(),
+ client=client,
+ )
+ finally:
+ sync_client.close()