Skip to content

Commit cfd35d3

Browse files
authored
Metadata: fix 401 when audio/transcriptions (#17023)
* Metadata: fix 401 when audio/transcriptions * check if str, CR fixes
1 parent 650b189 commit cfd35d3

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

litellm/proxy/common_utils/http_parsing_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ async def _read_request_body(request: Optional[Request]) -> Dict:
3939

4040
if "form" in content_type:
4141
parsed_body = dict(await request.form())
42+
if "metadata" in parsed_body and isinstance(parsed_body["metadata"], str):
43+
parsed_body["metadata"] = json.loads(parsed_body["metadata"])
4244
else:
4345
# Read the request body
4446
body = await request.body()

tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,208 @@ async def test_form_data_parsing():
9393
assert not hasattr(mock_request, "body") or not mock_request.body.called
9494

9595

96+
@pytest.mark.asyncio
97+
async def test_form_data_with_json_metadata():
98+
"""
99+
Test that form data with a JSON-encoded metadata field is correctly parsed.
100+
101+
When form data includes a 'metadata' field, it comes as a JSON string that needs
102+
to be parsed into a Python dictionary (lines 42-43 of http_parsing_utils.py).
103+
"""
104+
# Create a mock request with form data containing JSON metadata
105+
mock_request = MagicMock()
106+
107+
# Metadata is sent as a JSON string in form data
108+
metadata_json_string = json.dumps({
109+
"user_id": "12345",
110+
"request_type": "audio_transcription",
111+
"tags": ["urgent", "production"],
112+
"custom_field": {"nested": "value"}
113+
})
114+
115+
test_data = {
116+
"model": "whisper-1",
117+
"file": "audio.mp3",
118+
"metadata": metadata_json_string # This is a JSON string, not a dict
119+
}
120+
121+
# Mock the form method to return the test data as an awaitable
122+
mock_request.form = AsyncMock(return_value=test_data)
123+
mock_request.headers = {"content-type": "multipart/form-data"}
124+
mock_request.scope = {}
125+
126+
# Parse the form data
127+
result = await _read_request_body(mock_request)
128+
129+
# Verify the metadata was parsed from JSON string to dict
130+
assert "metadata" in result
131+
assert isinstance(result["metadata"], dict)
132+
assert result["metadata"]["user_id"] == "12345"
133+
assert result["metadata"]["request_type"] == "audio_transcription"
134+
assert result["metadata"]["tags"] == ["urgent", "production"]
135+
assert result["metadata"]["custom_field"] == {"nested": "value"}
136+
137+
# Verify other fields remain unchanged
138+
assert result["model"] == "whisper-1"
139+
assert result["file"] == "audio.mp3"
140+
141+
# Verify form() was called
142+
mock_request.form.assert_called_once()
143+
144+
145+
@pytest.mark.asyncio
146+
async def test_form_data_with_invalid_json_metadata():
147+
"""
148+
Test that form data with invalid JSON in metadata field raises an exception.
149+
150+
This tests error handling when the metadata field contains malformed JSON.
151+
"""
152+
# Create a mock request with form data containing invalid JSON metadata
153+
mock_request = MagicMock()
154+
155+
test_data = {
156+
"model": "whisper-1",
157+
"file": "audio.mp3",
158+
"metadata": '{"invalid": json}' # Invalid JSON - unquoted value
159+
}
160+
161+
# Mock the form method to return the test data
162+
mock_request.form = AsyncMock(return_value=test_data)
163+
mock_request.headers = {"content-type": "multipart/form-data"}
164+
mock_request.scope = {}
165+
166+
# Should raise JSONDecodeError when trying to parse invalid JSON metadata
167+
with pytest.raises(json.JSONDecodeError):
168+
await _read_request_body(mock_request)
169+
170+
171+
@pytest.mark.asyncio
172+
async def test_form_data_without_metadata():
173+
"""
174+
Test that form data without metadata field works correctly.
175+
176+
Ensures the metadata parsing logic doesn't break when metadata is absent.
177+
"""
178+
# Create a mock request with form data without metadata
179+
mock_request = MagicMock()
180+
181+
test_data = {
182+
"model": "whisper-1",
183+
"file": "audio.mp3",
184+
"language": "en"
185+
}
186+
187+
# Mock the form method to return the test data
188+
mock_request.form = AsyncMock(return_value=test_data)
189+
mock_request.headers = {"content-type": "application/x-www-form-urlencoded"}
190+
mock_request.scope = {}
191+
192+
# Parse the form data
193+
result = await _read_request_body(mock_request)
194+
195+
# Verify all fields are preserved as-is
196+
assert result == test_data
197+
assert "metadata" not in result
198+
assert result["model"] == "whisper-1"
199+
assert result["file"] == "audio.mp3"
200+
assert result["language"] == "en"
201+
202+
203+
@pytest.mark.asyncio
204+
async def test_form_data_with_empty_metadata():
205+
"""
206+
Test that form data with empty JSON object in metadata field is parsed correctly.
207+
"""
208+
# Create a mock request with form data containing empty metadata
209+
mock_request = MagicMock()
210+
211+
test_data = {
212+
"model": "whisper-1",
213+
"file": "audio.mp3",
214+
"metadata": "{}" # Empty JSON object as string
215+
}
216+
217+
# Mock the form method to return the test data
218+
mock_request.form = AsyncMock(return_value=test_data)
219+
mock_request.headers = {"content-type": "multipart/form-data"}
220+
mock_request.scope = {}
221+
222+
# Parse the form data
223+
result = await _read_request_body(mock_request)
224+
225+
# Verify the metadata was parsed to an empty dict
226+
assert "metadata" in result
227+
assert isinstance(result["metadata"], dict)
228+
assert result["metadata"] == {}
229+
assert result["model"] == "whisper-1"
230+
231+
232+
@pytest.mark.asyncio
233+
async def test_form_data_with_dict_metadata():
234+
"""
235+
Test that form data with metadata already as a dict is not parsed again.
236+
237+
This handles edge cases where metadata might already be a dictionary
238+
(shouldn't happen in normal form data, but defensive coding).
239+
"""
240+
# Create a mock request with form data where metadata is already a dict
241+
mock_request = MagicMock()
242+
243+
metadata_dict = {
244+
"user_id": "12345",
245+
"tags": ["test"]
246+
}
247+
248+
test_data = {
249+
"model": "whisper-1",
250+
"file": "audio.mp3",
251+
"metadata": metadata_dict # Already a dict, not a string
252+
}
253+
254+
# Mock the form method to return the test data
255+
mock_request.form = AsyncMock(return_value=test_data)
256+
mock_request.headers = {"content-type": "multipart/form-data"}
257+
mock_request.scope = {}
258+
259+
# Parse the form data
260+
result = await _read_request_body(mock_request)
261+
262+
# Verify the metadata remains as a dict and is not parsed
263+
assert "metadata" in result
264+
assert isinstance(result["metadata"], dict)
265+
assert result["metadata"] == metadata_dict
266+
assert result["metadata"]["user_id"] == "12345"
267+
assert result["model"] == "whisper-1"
268+
269+
270+
@pytest.mark.asyncio
271+
async def test_form_data_with_none_metadata():
272+
"""
273+
Test that form data with None metadata value is handled gracefully.
274+
"""
275+
# Create a mock request with form data where metadata is None
276+
mock_request = MagicMock()
277+
278+
test_data = {
279+
"model": "whisper-1",
280+
"file": "audio.mp3",
281+
"metadata": None # None value
282+
}
283+
284+
# Mock the form method to return the test data
285+
mock_request.form = AsyncMock(return_value=test_data)
286+
mock_request.headers = {"content-type": "multipart/form-data"}
287+
mock_request.scope = {}
288+
289+
# Parse the form data
290+
result = await _read_request_body(mock_request)
291+
292+
# Verify the metadata remains None (not parsed)
293+
assert "metadata" in result
294+
assert result["metadata"] is None
295+
assert result["model"] == "whisper-1"
296+
297+
96298
@pytest.mark.asyncio
97299
async def test_empty_request_body():
98300
"""

0 commit comments

Comments
 (0)