mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): decode multipart user_config JSON
This commit is contained in:
parent
2f22a1293e
commit
b63721c45f
2 changed files with 69 additions and 2 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue