fix(proxy): decode multipart user_config JSON

This commit is contained in:
Kcstring 2026-04-29 17:11:03 +08:00
parent 2f22a1293e
commit b63721c45f
2 changed files with 69 additions and 2 deletions

View file

@ -13,6 +13,16 @@ from litellm.proxy.common_utils.callback_utils import (
from litellm.types.router import Deployment
def _parse_form_json_field(
parsed_body: Dict[str, Any], field_name: str, *, require_json: bool = False
) -> None:
value = parsed_body.get(field_name)
if not isinstance(value, str):
return
if require_json or value.lstrip().startswith(("{", "[")):
parsed_body[field_name] = json.loads(value)
async def _read_request_body(request: Optional[Request]) -> Dict:
"""
Safely read the request body and parse it as JSON.
@ -39,8 +49,9 @@ async def _read_request_body(request: Optional[Request]) -> Dict:
if "form" in content_type:
parsed_body = dict(await request.form())
if "metadata" in parsed_body and isinstance(parsed_body["metadata"], str):
parsed_body["metadata"] = json.loads(parsed_body["metadata"])
_parse_form_json_field(parsed_body, "metadata", require_json=True)
_parse_form_json_field(parsed_body, "user_config")
_parse_form_json_field(parsed_body, "tags")
else:
# Read the request body
body = await request.body()

View file

@ -0,0 +1,56 @@
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
@pytest.mark.asyncio
async def test_form_data_with_json_user_config_and_tags():
mock_request = MagicMock()
user_config = {
"model_list": [
{
"model_name": "openai/gpt-image-1",
"litellm_params": {
"model": "openai/gpt-image-1",
"api_key": "sk-fake",
},
}
]
}
test_data = {
"model": "openai/gpt-image-1",
"prompt": "test",
"user_config": json.dumps(user_config),
"tags": json.dumps(["image-edit", "multipart"]),
}
mock_request.form = AsyncMock(return_value=test_data)
mock_request.headers = {"content-type": "multipart/form-data"}
mock_request.scope = {}
mock_request.state._cached_headers = None
result = await _read_request_body(mock_request)
assert result["user_config"] == user_config
assert result["tags"] == ["image-edit", "multipart"]
assert result["model"] == "openai/gpt-image-1"
assert result["prompt"] == "test"
mock_request.form.assert_called_once()
@pytest.mark.asyncio
async def test_form_data_with_plain_string_tags_is_left_unchanged():
mock_request = MagicMock()
test_data = {"model": "whisper-1", "tags": "production"}
mock_request.form = AsyncMock(return_value=test_data)
mock_request.headers = {"content-type": "multipart/form-data"}
mock_request.scope = {}
mock_request.state._cached_headers = None
result = await _read_request_body(mock_request)
assert result["tags"] == "production"