mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-15 04:19:08 +00:00
fix(bedrock): return metadata for empty S3 objects
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9721d9f82b
commit
a04b2437e4
5 changed files with 183 additions and 4 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"<Error><Code>InvalidRange</Code><ActualObjectSize>0</ActualObjectSize></Error>",
|
||||
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"<Error><Code>InvalidRange</Code></Error>",
|
||||
request=httpx.Request("GET", self.S3_URI),
|
||||
),
|
||||
logging_obj=MagicMock(),
|
||||
|
|
|
|||
|
|
@ -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"<Error><Code>InvalidRange</Code><ActualObjectSize>0</ActualObjectSize></Error>",
|
||||
{},
|
||||
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"<Error><Code>InvalidRange</Code></Error>",
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue