mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-10-04 04:04:14 +00:00
Add Form() annotations and fix OpenAPI generation for multipart params
- Add Form() annotations to purpose and expires_after parameters in file upload endpoints - Add support for optional multipart form parameters in OpenAPI generator - Generated spec now properly mirrors OpenAI format with schema refs
This commit is contained in:
parent
8288122146
commit
f676c48a97
8 changed files with 705 additions and 362 deletions
|
@ -7,7 +7,7 @@
|
|||
from enum import StrEnum
|
||||
from typing import Annotated, ClassVar, Literal, Protocol, runtime_checkable
|
||||
|
||||
from fastapi import File, Response, UploadFile
|
||||
from fastapi import File, Form, Response, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from llama_stack.apis.common.responses import Order
|
||||
|
@ -110,9 +110,8 @@ class Files(Protocol):
|
|||
async def openai_upload_file(
|
||||
self,
|
||||
file: Annotated[UploadFile, File()],
|
||||
purpose: OpenAIFilePurpose,
|
||||
expires_after: ExpiresAfter | None = None,
|
||||
# TODO: expires_after is producing strange openapi spec, params are showing up as a required w/ oneOf being null
|
||||
purpose: Annotated[OpenAIFilePurpose, Form()],
|
||||
expires_after: Annotated[ExpiresAfter | None, Form()] = None,
|
||||
) -> OpenAIFileObject:
|
||||
"""
|
||||
Upload a file that can be used across various endpoints.
|
||||
|
|
|
@ -9,7 +9,7 @@ import uuid
|
|||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import File, Response, UploadFile
|
||||
from fastapi import File, Form, Response, UploadFile
|
||||
|
||||
from llama_stack.apis.common.errors import ResourceNotFoundError
|
||||
from llama_stack.apis.common.responses import Order
|
||||
|
@ -86,8 +86,8 @@ class LocalfsFilesImpl(Files):
|
|||
async def openai_upload_file(
|
||||
self,
|
||||
file: Annotated[UploadFile, File()],
|
||||
purpose: OpenAIFilePurpose,
|
||||
expires_after: ExpiresAfter | None = None,
|
||||
purpose: Annotated[OpenAIFilePurpose, Form()],
|
||||
expires_after: Annotated[ExpiresAfter | None, Form()] = None,
|
||||
) -> OpenAIFileObject:
|
||||
"""Upload a file that can be used across various endpoints."""
|
||||
if not self.sql_store:
|
||||
|
|
|
@ -10,7 +10,7 @@ from typing import Annotated, Any
|
|||
|
||||
import boto3
|
||||
from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError
|
||||
from fastapi import File, Response, UploadFile
|
||||
from fastapi import File, Form, Response, UploadFile
|
||||
|
||||
from llama_stack.apis.common.errors import ResourceNotFoundError
|
||||
from llama_stack.apis.common.responses import Order
|
||||
|
@ -194,8 +194,8 @@ class S3FilesImpl(Files):
|
|||
async def openai_upload_file(
|
||||
self,
|
||||
file: Annotated[UploadFile, File()],
|
||||
purpose: OpenAIFilePurpose,
|
||||
expires_after: ExpiresAfter | None = None,
|
||||
purpose: Annotated[OpenAIFilePurpose, Form()],
|
||||
expires_after: Annotated[ExpiresAfter | None, Form()] = None,
|
||||
) -> OpenAIFileObject:
|
||||
file_id = f"file-{uuid.uuid4().hex}"
|
||||
|
||||
|
|
|
@ -567,6 +567,20 @@ def get_class_properties(typ: type) -> Iterable[Tuple[str, type | str]]:
|
|||
|
||||
if is_dataclass_type(typ):
|
||||
return ((field.name, field.type) for field in dataclasses.fields(typ))
|
||||
elif hasattr(typ, 'model_fields'):
|
||||
# Pydantic BaseModel - use model_fields to exclude ClassVar and other non-field attributes
|
||||
# Reconstruct Annotated type if discriminator exists to preserve metadata
|
||||
from typing import Annotated
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
def get_field_type(name: str, field) -> type | str:
|
||||
# If field has discriminator, wrap in Annotated to preserve it for schema generation
|
||||
if field.discriminator:
|
||||
field_info = FieldInfo(annotation=None, discriminator=field.discriminator)
|
||||
return Annotated[field.annotation, field_info]
|
||||
return field.annotation
|
||||
|
||||
return ((name, get_field_type(name, field)) for name, field in typ.model_fields.items())
|
||||
else:
|
||||
resolved_hints = get_resolved_hints(typ)
|
||||
return resolved_hints.items()
|
||||
|
|
|
@ -93,6 +93,11 @@ def get_class_property_docstrings(
|
|||
"""
|
||||
|
||||
result = {}
|
||||
# Only try to get MRO if data_type is actually a class
|
||||
# Special types like Literal, Union, etc. don't have MRO
|
||||
if not inspect.isclass(data_type):
|
||||
return result
|
||||
|
||||
for base in inspect.getmro(data_type):
|
||||
docstr = docstring.parse_type(base)
|
||||
for param in docstr.params.values():
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue