mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge pull request #36392 from BerriAI/devin_ai_fix_bedrock_batch_file_bytes_36388
fix(bedrock): report uploaded size in the FileObject returned by managed batch uploads
This commit is contained in:
commit
a1644eaf84
2 changed files with 73 additions and 7 deletions
|
|
@ -64,6 +64,10 @@ from ..common_utils import BedrockError, merge_bedrock_aws_request_params, resol
|
|||
# Same pattern as the `upload_url` handoff in `transform_create_file_request`.
|
||||
S3_SIGNED_GET_HEADERS_PARAM: Final = "_s3_signed_get_headers"
|
||||
|
||||
# litellm_params key carrying the size of the body uploaded to S3, handed from
|
||||
# `transform_create_file_request` to `transform_create_file_response`.
|
||||
UPLOAD_CONTENT_LENGTH_PARAM: Final = "_s3_upload_content_length"
|
||||
|
||||
|
||||
def _frozen_mapping(items: Iterable[tuple[str, object]]) -> Mapping[str, object]:
|
||||
return MappingProxyType(dict(items))
|
||||
|
|
@ -197,6 +201,18 @@ def get_configured_s3_bucket_name(litellm_params: Mapping[str, object]) -> str:
|
|||
return bucket_name
|
||||
|
||||
|
||||
def _uploaded_object_size(litellm_params: Mapping[str, object], raw_response: Response) -> int:
|
||||
"""
|
||||
S3 answers PutObject with an empty body, so the stored object size comes from the
|
||||
signed request recorded by `transform_create_file_request`, not the response headers.
|
||||
"""
|
||||
uploaded_size: Final = litellm_params.get(UPLOAD_CONTENT_LENGTH_PARAM)
|
||||
if isinstance(uploaded_size, int):
|
||||
return uploaded_size
|
||||
response_content_length: Final = raw_response.headers.get("Content-Length", "0")
|
||||
return int(response_content_length) if response_content_length.isdigit() else 0
|
||||
|
||||
|
||||
class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
||||
"""
|
||||
Config for Bedrock Files - handles S3 uploads for Bedrock batch processing
|
||||
|
|
@ -924,6 +940,8 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
|||
)
|
||||
|
||||
litellm_params["upload_url"] = api_base
|
||||
upload_content_length: Final = len(file_content.encode("utf-8"))
|
||||
litellm_params[UPLOAD_CONTENT_LENGTH_PARAM] = upload_content_length # rebind-ok: same handoff as upload_url
|
||||
|
||||
# Return a dict that tells the HTTP handler exactly what to do
|
||||
return {
|
||||
|
|
@ -1081,12 +1099,6 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
|||
"""
|
||||
Transform S3 File upload response into OpenAI-style FileObject
|
||||
"""
|
||||
# For S3 uploads, we typically get an ETag and other metadata
|
||||
response_headers: Final = raw_response.headers
|
||||
# Extract S3 object information from the response
|
||||
# S3 PUT object returns ETag and other metadata in headers
|
||||
content_length: Final[str] = response_headers.get("Content-Length", "0")
|
||||
|
||||
# Use the actual upload URL that was used for the S3 upload
|
||||
upload_url: Final = litellm_params.get("upload_url")
|
||||
file_id: str = ""
|
||||
|
|
@ -1101,7 +1113,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
|||
filename=filename,
|
||||
created_at=int(time.time()), # Current timestamp
|
||||
status="uploaded",
|
||||
bytes=int(content_length) if content_length.isdigit() else 0,
|
||||
bytes=_uploaded_object_size(litellm_params=litellm_params, raw_response=raw_response),
|
||||
object="file",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -586,6 +586,60 @@ class TestBedrockFilesTransformation:
|
|||
assert "x-amz-server-side-encryption" not in headers
|
||||
assert "x-amz-server-side-encryption-aws-kms-key-id" not in headers
|
||||
|
||||
def test_create_file_response_reports_uploaded_object_size(self):
|
||||
"""
|
||||
S3 answers PutObject with an empty body, so the returned FileObject must report the
|
||||
size of the body that was uploaded instead of the response's Content-Length (always 0).
|
||||
"""
|
||||
import httpx
|
||||
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
||||
config = BedrockFilesConfig()
|
||||
litellm_params: dict = {"s3_bucket_name": "litellm-batch-bucket"}
|
||||
jsonl_content = json.dumps(
|
||||
{
|
||||
"custom_id": "req-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": "bedrock/amazon.nova-pro-v1:0",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"max_tokens": 10,
|
||||
},
|
||||
}
|
||||
).encode()
|
||||
|
||||
request = config.transform_create_file_request(
|
||||
model="amazon.nova-pro-v1:0",
|
||||
create_file_data={
|
||||
"file": ("batch.jsonl", jsonl_content, "application/jsonl"),
|
||||
"purpose": "batch",
|
||||
},
|
||||
optional_params={
|
||||
"aws_access_key_id": "test-key-id",
|
||||
"aws_secret_access_key": "test-secret",
|
||||
"aws_region_name": "us-west-2",
|
||||
},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
assert isinstance(request, dict)
|
||||
uploaded_size = len(request["data"].encode("utf-8"))
|
||||
assert uploaded_size > 0
|
||||
|
||||
file_object = config.transform_create_file_response(
|
||||
model=None,
|
||||
raw_response=httpx.Response(
|
||||
status_code=200,
|
||||
headers={"Content-Length": "0", "ETag": '"abc123"'},
|
||||
content=b"",
|
||||
),
|
||||
logging_obj=MagicMock(),
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
assert file_object.bytes == uploaded_size
|
||||
|
||||
def test_openai_passthrough_still_works(self):
|
||||
"""
|
||||
Regression test: ensure OpenAI-compatible models (e.g. gpt-oss)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue