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),
)