mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): keep every repeated form field value in get_form_data
dict(form) collapses duplicate multipart keys to the last one, so a request with timestamp_granularities[]=segment and timestamp_granularities[]=word reached the provider as a single-element list. Iterate form.multi_items() instead. Fixes #35937 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
b735578822
commit
c1f005bbd4
3 changed files with 37 additions and 23 deletions
|
|
@ -274,9 +274,8 @@ async def get_form_data(request: Request) -> dict[str, Any]:
|
|||
Handles when OpenAI SDKs pass form keys as `timestamp_granularities[]="word"` instead of `timestamp_granularities=["word", "sentence"]`
|
||||
"""
|
||||
form: Final = await request.form()
|
||||
form_data: Final = dict(form)
|
||||
parsed_form_data: Final[dict[str, Any]] = {}
|
||||
for key, value in form_data.items():
|
||||
for key, value in form.multi_items():
|
||||
# OpenAI SDKs pass form keys as `timestamp_granularities[]="word"` instead of `timestamp_granularities=["word", "sentence"]`
|
||||
if key.endswith("[]"):
|
||||
clean_key = key[:-2]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import orjson
|
|||
import pytest
|
||||
from fastapi import Request
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.datastructures import FormData
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../..")
|
||||
|
|
@ -506,19 +507,20 @@ async def test_get_form_data():
|
|||
# Create a mock request with transcription form data
|
||||
mock_request = MagicMock()
|
||||
|
||||
# Create mock form data with array notation for timestamp_granularities
|
||||
mock_form_data = {
|
||||
"file": "file_object", # In a real request this would be an UploadFile
|
||||
"model": "gpt-4o-transcribe",
|
||||
"include[]": "logprobs", # Array notation
|
||||
"language": "en",
|
||||
"prompt": "Transcribe this audio file",
|
||||
"response_format": "json",
|
||||
"stream": "false",
|
||||
"temperature": "0.2",
|
||||
"timestamp_granularities[]": "word", # First array item
|
||||
"timestamp_granularities[]": "segment", # Second array item (would overwrite in dict, but handled by the function)
|
||||
}
|
||||
mock_form_data = FormData(
|
||||
[
|
||||
("file", "file_object"), # In a real request this would be an UploadFile
|
||||
("model", "gpt-4o-transcribe"),
|
||||
("include[]", "logprobs"),
|
||||
("language", "en"),
|
||||
("prompt", "Transcribe this audio file"),
|
||||
("response_format", "json"),
|
||||
("stream", "false"),
|
||||
("temperature", "0.2"),
|
||||
("timestamp_granularities[]", "word"),
|
||||
("timestamp_granularities[]", "segment"),
|
||||
]
|
||||
)
|
||||
|
||||
# Mock the form method to return the test data
|
||||
mock_request.form = AsyncMock(return_value=mock_form_data)
|
||||
|
|
@ -540,11 +542,23 @@ async def test_get_form_data():
|
|||
assert isinstance(result["include"], list)
|
||||
assert "logprobs" in result["include"]
|
||||
|
||||
assert "timestamp_granularities" in result
|
||||
assert isinstance(result["timestamp_granularities"], list)
|
||||
# Note: In a real MultiDict, both values would be present
|
||||
# But in our mock dictionary the second value overwrites the first
|
||||
assert "segment" in result["timestamp_granularities"]
|
||||
assert result["timestamp_granularities"] == ["word", "segment"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"granularities",
|
||||
[["segment", "word"], ["word", "segment"], ["segment"], ["word"]],
|
||||
)
|
||||
async def test_get_form_data_preserves_every_repeated_array_value(granularities):
|
||||
mock_request = MagicMock()
|
||||
mock_request.form = AsyncMock(
|
||||
return_value=FormData([("model", "whisper-1"), *(("timestamp_granularities[]", g) for g in granularities)])
|
||||
)
|
||||
|
||||
result = await get_form_data(mock_request)
|
||||
|
||||
assert result["timestamp_granularities"] == granularities
|
||||
|
||||
|
||||
def test_get_tags_from_request_body_with_metadata_tags():
|
||||
|
|
@ -969,7 +983,7 @@ class TestReadRequestBodyNonCanonicalContentType:
|
|||
@pytest.mark.asyncio
|
||||
async def test_real_form_post_still_parsed_as_form(self):
|
||||
mock_request = MagicMock()
|
||||
mock_request.form = AsyncMock(return_value={"k": "v"})
|
||||
mock_request.form = AsyncMock(return_value=FormData([("k", "v")]))
|
||||
mock_request.body = AsyncMock(return_value=b"")
|
||||
mock_request.headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
mock_request.scope = {}
|
||||
|
|
@ -1025,7 +1039,7 @@ class TestGetRequestBody:
|
|||
mock_request = MagicMock()
|
||||
mock_request.method = "POST"
|
||||
mock_request.headers = {"content-type": "multipart/form-data; boundary=x"}
|
||||
mock_request.form = AsyncMock(return_value={"k": "v"})
|
||||
mock_request.form = AsyncMock(return_value=FormData([("k", "v")]))
|
||||
mock_request.scope = {}
|
||||
|
||||
result = await get_request_body(mock_request)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import httpx
|
|||
import pytest
|
||||
from fastapi import Request, Response
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.datastructures import FormData
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../..")
|
||||
|
|
@ -1259,7 +1260,7 @@ async def test_is_streaming_request_fn():
|
|||
mock_request = Mock()
|
||||
mock_request.method = "POST"
|
||||
mock_request.headers = {"content-type": "multipart/form-data"}
|
||||
mock_request.form = AsyncMock(return_value={"stream": "true"})
|
||||
mock_request.form = AsyncMock(return_value=FormData([("stream", "true")]))
|
||||
assert await is_streaming_request_fn(mock_request) is True
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue