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:
devin-ai-integration[bot] 2026-08-05 12:13:11 +00:00 committed by GitHub
parent b735578822
commit c1f005bbd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 37 additions and 23 deletions

View file

@ -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]

View file

@ -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)

View file

@ -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