diff --git a/docs/static/llama-stack-spec.html b/docs/static/llama-stack-spec.html index 2072af745..c0c1168d8 100644 --- a/docs/static/llama-stack-spec.html +++ b/docs/static/llama-stack-spec.html @@ -6070,7 +6070,7 @@ "Files" ], "summary": "Upload a file that can be used across various endpoints.", - "description": "Upload a file that can be used across various endpoints.\nThe file upload should be a multipart form request with:\n- file: The File object (not file name) to be uploaded.\n- purpose: The intended purpose of the uploaded file.\n- expires_after: Optional form values describing expiration for the file. Expected expires_after[anchor] = \"created_at\", expires_after[seconds] = {integer}. Seconds must be between 3600 and 2592000 (1 hour to 30 days).", + "description": "Upload a file that can be used across various endpoints.\nThe file upload should be a multipart form request with:\n- file: The File object (not file name) to be uploaded.\n- purpose: The intended purpose of the uploaded file.\n- expires_after: Optional form values describing expiration for the file.", "parameters": [], "requestBody": { "content": { @@ -6081,36 +6081,10 @@ "file": { "type": "string", "format": "binary" - }, - "purpose": { - "$ref": "#/components/schemas/OpenAIFilePurpose" - }, - "expires_after_anchor": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "expires_after_seconds": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ] } }, "required": [ - "file", - "purpose", - "expires_after_anchor", - "expires_after_seconds" + "file" ] } } @@ -6218,7 +6192,7 @@ "Files" ], "summary": "Upload a file that can be used across various endpoints.", - "description": "Upload a file that can be used across various endpoints.\nThe file upload should be a multipart form request with:\n- file: The File object (not file name) to be uploaded.\n- purpose: The intended purpose of the uploaded file.\n- expires_after: Optional form values describing expiration for the file. Expected expires_after[anchor] = \"created_at\", expires_after[seconds] = {integer}. Seconds must be between 3600 and 2592000 (1 hour to 30 days).", + "description": "Upload a file that can be used across various endpoints.\nThe file upload should be a multipart form request with:\n- file: The File object (not file name) to be uploaded.\n- purpose: The intended purpose of the uploaded file.\n- expires_after: Optional form values describing expiration for the file.", "parameters": [], "requestBody": { "content": { @@ -6229,36 +6203,10 @@ "file": { "type": "string", "format": "binary" - }, - "purpose": { - "$ref": "#/components/schemas/OpenAIFilePurpose" - }, - "expires_after_anchor": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "expires_after_seconds": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ] } }, "required": [ - "file", - "purpose", - "expires_after_anchor", - "expires_after_seconds" + "file" ] } } diff --git a/docs/static/llama-stack-spec.yaml b/docs/static/llama-stack-spec.yaml index 7b51116ba..5e3a44813 100644 --- a/docs/static/llama-stack-spec.yaml +++ b/docs/static/llama-stack-spec.yaml @@ -4383,8 +4383,6 @@ paths: - purpose: The intended purpose of the uploaded file. - expires_after: Optional form values describing expiration for the file. - Expected expires_after[anchor] = "created_at", expires_after[seconds] = {integer}. - Seconds must be between 3600 and 2592000 (1 hour to 30 days). parameters: [] requestBody: content: @@ -4395,21 +4393,8 @@ paths: file: type: string format: binary - purpose: - $ref: '#/components/schemas/OpenAIFilePurpose' - expires_after_anchor: - oneOf: - - type: string - - type: 'null' - expires_after_seconds: - oneOf: - - type: integer - - type: 'null' required: - file - - purpose - - expires_after_anchor - - expires_after_seconds required: true /v1/openai/v1/files: get: @@ -4504,8 +4489,6 @@ paths: - purpose: The intended purpose of the uploaded file. - expires_after: Optional form values describing expiration for the file. - Expected expires_after[anchor] = "created_at", expires_after[seconds] = {integer}. - Seconds must be between 3600 and 2592000 (1 hour to 30 days). parameters: [] requestBody: content: @@ -4516,21 +4499,8 @@ paths: file: type: string format: binary - purpose: - $ref: '#/components/schemas/OpenAIFilePurpose' - expires_after_anchor: - oneOf: - - type: string - - type: 'null' - expires_after_seconds: - oneOf: - - type: integer - - type: 'null' required: - file - - purpose - - expires_after_anchor - - expires_after_seconds required: true /v1/openai/v1/models: get: diff --git a/llama_stack/apis/files/files.py b/llama_stack/apis/files/files.py index d5abb6286..7747e60f6 100644 --- a/llama_stack/apis/files/files.py +++ b/llama_stack/apis/files/files.py @@ -110,9 +110,8 @@ class Files(Protocol): async def openai_upload_file( self, file: Annotated[UploadFile, File()], - purpose: Annotated[OpenAIFilePurpose, Form()], - expires_after_anchor: Annotated[str | None, Form(alias="expires_after[anchor]")] = None, - expires_after_seconds: Annotated[int | None, Form(alias="expires_after[seconds]")] = None, + 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 ) -> OpenAIFileObject: """ @@ -121,10 +120,11 @@ class Files(Protocol): The file upload should be a multipart form request with: - file: The File object (not file name) to be uploaded. - purpose: The intended purpose of the uploaded file. - - expires_after: Optional form values describing expiration for the file. Expected expires_after[anchor] = "created_at", expires_after[seconds] = {integer}. Seconds must be between 3600 and 2592000 (1 hour to 30 days). + - expires_after: Optional form values describing expiration for the file. :param file: The uploaded file object containing content and metadata (filename, content_type, etc.). :param purpose: The intended purpose of the uploaded file (e.g., "assistants", "fine-tune"). + :param expires_after: Optional form values describing expiration for the file. :returns: An OpenAIFileObject representing the uploaded file. """ ... diff --git a/llama_stack/providers/inline/files/localfs/files.py b/llama_stack/providers/inline/files/localfs/files.py index 65cf8d815..6e0c72de3 100644 --- a/llama_stack/providers/inline/files/localfs/files.py +++ b/llama_stack/providers/inline/files/localfs/files.py @@ -14,6 +14,7 @@ from fastapi import File, Form, Response, UploadFile from llama_stack.apis.common.errors import ResourceNotFoundError from llama_stack.apis.common.responses import Order from llama_stack.apis.files import ( + ExpiresAfter, Files, ListOpenAIFileResponse, OpenAIFileDeleteResponse, @@ -86,14 +87,13 @@ class LocalfsFilesImpl(Files): self, file: Annotated[UploadFile, File()], purpose: Annotated[OpenAIFilePurpose, Form()], - expires_after_anchor: Annotated[str | None, Form(alias="expires_after[anchor]")] = None, - expires_after_seconds: Annotated[int | None, Form(alias="expires_after[seconds]")] = None, + expires_after: Annotated[ExpiresAfter | None, Form()] = None, ) -> OpenAIFileObject: """Upload a file that can be used across various endpoints.""" if not self.sql_store: raise RuntimeError("Files provider not initialized") - if expires_after_anchor is not None or expires_after_seconds is not None: + if expires_after is not None: raise NotImplementedError("File expiration is not supported by this provider") file_id = self._generate_file_id() diff --git a/llama_stack/providers/remote/files/s3/files.py b/llama_stack/providers/remote/files/s3/files.py index 8ea96af9e..8520f70b6 100644 --- a/llama_stack/providers/remote/files/s3/files.py +++ b/llama_stack/providers/remote/files/s3/files.py @@ -195,8 +195,7 @@ class S3FilesImpl(Files): self, file: Annotated[UploadFile, File()], purpose: Annotated[OpenAIFilePurpose, Form()], - expires_after_anchor: Annotated[str | None, Form(alias="expires_after[anchor]")] = None, - expires_after_seconds: Annotated[int | None, Form(alias="expires_after[seconds]")] = None, + expires_after: Annotated[ExpiresAfter | None, Form()] = None, ) -> OpenAIFileObject: file_id = f"file-{uuid.uuid4().hex}" @@ -204,14 +203,6 @@ class S3FilesImpl(Files): created_at = self._now() - expires_after = None - if expires_after_anchor is not None or expires_after_seconds is not None: - # we use ExpiresAfter to validate input - expires_after = ExpiresAfter( - anchor=expires_after_anchor, # type: ignore[arg-type] - seconds=expires_after_seconds, # type: ignore[arg-type] - ) - # the default is no expiration. # to implement no expiration we set an expiration beyond the max. # we'll hide this fact from users when returning the file object. diff --git a/tests/unit/providers/files/test_s3_files.py b/tests/unit/providers/files/test_s3_files.py index c665bf124..92a45a9f2 100644 --- a/tests/unit/providers/files/test_s3_files.py +++ b/tests/unit/providers/files/test_s3_files.py @@ -228,12 +228,13 @@ class TestS3FilesImpl: mock_now.return_value = 0 + from llama_stack.apis.files import ExpiresAfter + sample_text_file.filename = "test_expired_file" uploaded = await s3_provider.openai_upload_file( file=sample_text_file, purpose=OpenAIFilePurpose.ASSISTANTS, - expires_after_anchor="created_at", - expires_after_seconds=two_hours, + expires_after=ExpiresAfter(anchor="created_at", seconds=two_hours), ) mock_now.return_value = two_hours * 2 # fast forward 4 hours @@ -259,42 +260,44 @@ class TestS3FilesImpl: async def test_unsupported_expires_after_anchor(self, s3_provider, sample_text_file): """Unsupported anchor value should raise ValueError.""" + from llama_stack.apis.files import ExpiresAfter + sample_text_file.filename = "test_unsupported_expires_after_anchor" with pytest.raises(ValueError, match="Input should be 'created_at'"): await s3_provider.openai_upload_file( file=sample_text_file, purpose=OpenAIFilePurpose.ASSISTANTS, - expires_after_anchor="now", - expires_after_seconds=3600, + expires_after=ExpiresAfter(anchor="now", seconds=3600), # type: ignore ) async def test_nonint_expires_after_seconds(self, s3_provider, sample_text_file): """Non-integer seconds in expires_after should raise ValueError.""" + from llama_stack.apis.files import ExpiresAfter + sample_text_file.filename = "test_nonint_expires_after_seconds" with pytest.raises(ValueError, match="should be a valid integer"): await s3_provider.openai_upload_file( file=sample_text_file, purpose=OpenAIFilePurpose.ASSISTANTS, - expires_after_anchor="created_at", - expires_after_seconds="many", + expires_after=ExpiresAfter(anchor="created_at", seconds="many"), # type: ignore ) async def test_expires_after_seconds_out_of_bounds(self, s3_provider, sample_text_file): """Seconds outside allowed range should raise ValueError.""" + from llama_stack.apis.files import ExpiresAfter + with pytest.raises(ValueError, match="greater than or equal to 3600"): await s3_provider.openai_upload_file( file=sample_text_file, purpose=OpenAIFilePurpose.ASSISTANTS, - expires_after_anchor="created_at", - expires_after_seconds=3599, + expires_after=ExpiresAfter(anchor="created_at", seconds=3599), ) with pytest.raises(ValueError, match="less than or equal to 2592000"): await s3_provider.openai_upload_file( file=sample_text_file, purpose=OpenAIFilePurpose.ASSISTANTS, - expires_after_anchor="created_at", - expires_after_seconds=2592001, + expires_after=ExpiresAfter(anchor="created_at", seconds=2592001), )