diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index becf1e0abc2..1b6e360b062 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -6,6 +6,7 @@ import posixpath import traceback from base64 import b64encode from datetime import datetime +from itertools import groupby from typing import Any, Dict, List, Mapping, Optional, Tuple, Union, cast from urllib.parse import urlencode, urlparse @@ -499,21 +500,23 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): if isinstance(field_value, (StarletteUploadFile, UploadFile)) ] - field_names = tuple( - dict.fromkeys( - field_name - for field_name, field_value in form_items - if not isinstance(field_value, (StarletteUploadFile, UploadFile)) - ) + non_file_items = tuple( + (field_name, field_value) + for field_name, field_value in form_items + if not isinstance(field_value, (StarletteUploadFile, UploadFile)) ) + field_order = { + field_name: index + for index, field_name in enumerate( + dict.fromkeys(field_name for field_name, _ in non_file_items) + ) + } form_data_dict = { - name: [ - field_value - for field_name, field_value in form_items - if field_name == name - and not isinstance(field_value, (StarletteUploadFile, UploadFile)) - ] - for name in field_names + field_name: [value for _, value in group] + for field_name, group in groupby( + sorted(non_file_items, key=lambda item: field_order[item[0]]), + key=lambda item: item[0], + ) } # Remove content-type header - httpx will set it correctly with the new boundary diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index 42e90c823c2..f97e75f7ce3 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -3276,8 +3276,10 @@ async def test_multipart_passthrough_preserves_boundary(): async def mock_httpx_request(method, url, **kwargs): # Verify that files parameter is passed (not json) assert "files" in kwargs, "Files should be passed for multipart requests" - files = dict(kwargs["files"]) - assert "file" in files, "File field should be in files" + file_parts = [ + value for name, value in kwargs["files"] if name == "file" + ] + assert len(file_parts) == 1, "File field should be in files" # Verify content-type is NOT in headers (httpx will set it with correct boundary) headers = kwargs.get("headers", {}) @@ -3285,7 +3287,7 @@ async def test_multipart_passthrough_preserves_boundary(): "content-type" not in headers ), "content-type should be removed for multipart" - filename, content, content_type = files["file"] + filename, content, content_type = file_parts[0] assert filename == "test.txt" assert content == b"test file content" assert content_type == "text/plain"