mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-10-04 12:07:34 +00:00
https://github.com/llamastack/llama-stack/pull/3604 broke multipart form data field parsing for the Files API since it changed its shape -- so as to match the API exactly to the OpenAI spec even in the generated client code. The underlying reason is that multipart/form-data cannot transport structured nested fields. Each field must be str-serialized. The client (specifically the OpenAI client whose behavior we must match), transports sub-fields as `expires_after[anchor]` and `expires_after[seconds]`, etc. We must be able to handle these fields somehow on the server without compromising the shape of the YAML spec. This PR "fixes" this by adding a dependency to convert the data. The main trade-off here is that we must add this `Depends()` annotation on every provider implementation for Files. This is a headache, but a much more reasonable one (in my opinion) given the alternatives. ## Test Plan Tests as shown in https://github.com/llamastack/llama-stack/pull/3604#issuecomment-3351090653 pass.
179 lines
5.5 KiB
Python
179 lines
5.5 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the terms described in the LICENSE file in
|
|
# the root directory of this source tree.
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from llama_stack.providers.utils.files.form_data import (
|
|
parse_expires_after,
|
|
parse_pydantic_from_form,
|
|
)
|
|
|
|
|
|
class _TestModel(BaseModel):
|
|
"""Simple test model for generic parsing tests."""
|
|
|
|
name: str
|
|
value: int
|
|
|
|
|
|
async def test_parse_pydantic_from_form_bracket_notation():
|
|
"""Test parsing a Pydantic model using bracket notation."""
|
|
# Create mock request with form data
|
|
mock_request = MagicMock()
|
|
mock_form = {
|
|
"test_field[name]": "test_name",
|
|
"test_field[value]": "42",
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is not None
|
|
assert result.name == "test_name"
|
|
assert result.value == 42
|
|
|
|
|
|
async def test_parse_pydantic_from_form_json_string():
|
|
"""Test parsing a Pydantic model from JSON string."""
|
|
# Create mock request with form data
|
|
mock_request = MagicMock()
|
|
test_data = {"name": "test_name", "value": 42}
|
|
mock_form = {
|
|
"test_field": json.dumps(test_data),
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is not None
|
|
assert result.name == "test_name"
|
|
assert result.value == 42
|
|
|
|
|
|
async def test_parse_pydantic_from_form_bracket_takes_precedence():
|
|
"""Test that bracket notation takes precedence over JSON string."""
|
|
# Create mock request with both formats
|
|
mock_request = MagicMock()
|
|
mock_form = {
|
|
"test_field[name]": "bracket_name",
|
|
"test_field[value]": "100",
|
|
"test_field": json.dumps({"name": "json_name", "value": 50}),
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is not None
|
|
# Bracket notation should win
|
|
assert result.name == "bracket_name"
|
|
assert result.value == 100
|
|
|
|
|
|
async def test_parse_pydantic_from_form_missing_field():
|
|
"""Test that None is returned when field is missing."""
|
|
# Create mock request with empty form
|
|
mock_request = MagicMock()
|
|
mock_form = {}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is None
|
|
|
|
|
|
async def test_parse_pydantic_from_form_invalid_json():
|
|
"""Test that None is returned for invalid JSON."""
|
|
# Create mock request with invalid JSON
|
|
mock_request = MagicMock()
|
|
mock_form = {
|
|
"test_field": "not valid json",
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is None
|
|
|
|
|
|
async def test_parse_pydantic_from_form_invalid_data():
|
|
"""Test that None is returned when data doesn't match model."""
|
|
# Create mock request with data that doesn't match the model
|
|
mock_request = MagicMock()
|
|
mock_form = {
|
|
"test_field[wrong_field]": "value",
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is None
|
|
|
|
|
|
async def test_parse_expires_after_bracket_notation():
|
|
"""Test parsing expires_after using bracket notation."""
|
|
# Create mock request with form data
|
|
mock_request = MagicMock()
|
|
mock_form = {
|
|
"expires_after[anchor]": "created_at",
|
|
"expires_after[seconds]": "3600",
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_expires_after(mock_request)
|
|
|
|
assert result is not None
|
|
assert result.anchor == "created_at"
|
|
assert result.seconds == 3600
|
|
|
|
|
|
async def test_parse_expires_after_json_string():
|
|
"""Test parsing expires_after from JSON string."""
|
|
# Create mock request with form data
|
|
mock_request = MagicMock()
|
|
expires_data = {"anchor": "created_at", "seconds": 7200}
|
|
mock_form = {
|
|
"expires_after": json.dumps(expires_data),
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_expires_after(mock_request)
|
|
|
|
assert result is not None
|
|
assert result.anchor == "created_at"
|
|
assert result.seconds == 7200
|
|
|
|
|
|
async def test_parse_expires_after_missing():
|
|
"""Test that None is returned when expires_after is missing."""
|
|
# Create mock request with empty form
|
|
mock_request = MagicMock()
|
|
mock_form = {}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_expires_after(mock_request)
|
|
|
|
assert result is None
|
|
|
|
|
|
async def test_parse_pydantic_from_form_type_conversion():
|
|
"""Test that bracket notation properly handles type conversion."""
|
|
# Create mock request with string values that need conversion
|
|
mock_request = MagicMock()
|
|
mock_form = {
|
|
"test_field[name]": "test",
|
|
"test_field[value]": "999", # String that should be converted to int
|
|
}
|
|
mock_request.form = AsyncMock(return_value=mock_form)
|
|
|
|
result = await parse_pydantic_from_form(mock_request, "test_field", _TestModel)
|
|
|
|
assert result is not None
|
|
assert result.name == "test"
|
|
assert result.value == 999
|
|
assert isinstance(result.value, int)
|