mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-10-04 04:04:14 +00:00
feat(files): fix expires_after API shape
This commit is contained in:
parent
5e7fed8bbb
commit
2a8391ec23
6 changed files with 25 additions and 113 deletions
60
docs/static/llama-stack-spec.html
vendored
60
docs/static/llama-stack-spec.html
vendored
|
@ -6070,7 +6070,7 @@
|
||||||
"Files"
|
"Files"
|
||||||
],
|
],
|
||||||
"summary": "Upload a file that can be used across various endpoints.",
|
"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": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -6081,36 +6081,10 @@
|
||||||
"file": {
|
"file": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "binary"
|
"format": "binary"
|
||||||
},
|
|
||||||
"purpose": {
|
|
||||||
"$ref": "#/components/schemas/OpenAIFilePurpose"
|
|
||||||
},
|
|
||||||
"expires_after_anchor": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"expires_after_seconds": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"file",
|
"file"
|
||||||
"purpose",
|
|
||||||
"expires_after_anchor",
|
|
||||||
"expires_after_seconds"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6218,7 +6192,7 @@
|
||||||
"Files"
|
"Files"
|
||||||
],
|
],
|
||||||
"summary": "Upload a file that can be used across various endpoints.",
|
"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": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -6229,36 +6203,10 @@
|
||||||
"file": {
|
"file": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "binary"
|
"format": "binary"
|
||||||
},
|
|
||||||
"purpose": {
|
|
||||||
"$ref": "#/components/schemas/OpenAIFilePurpose"
|
|
||||||
},
|
|
||||||
"expires_after_anchor": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"expires_after_seconds": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"file",
|
"file"
|
||||||
"purpose",
|
|
||||||
"expires_after_anchor",
|
|
||||||
"expires_after_seconds"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
docs/static/llama-stack-spec.yaml
vendored
30
docs/static/llama-stack-spec.yaml
vendored
|
@ -4383,8 +4383,6 @@ paths:
|
||||||
- purpose: The intended purpose of the uploaded file.
|
- purpose: The intended purpose of the uploaded file.
|
||||||
|
|
||||||
- expires_after: Optional form values describing expiration for the 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: []
|
parameters: []
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
|
@ -4395,21 +4393,8 @@ paths:
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
purpose:
|
|
||||||
$ref: '#/components/schemas/OpenAIFilePurpose'
|
|
||||||
expires_after_anchor:
|
|
||||||
oneOf:
|
|
||||||
- type: string
|
|
||||||
- type: 'null'
|
|
||||||
expires_after_seconds:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: 'null'
|
|
||||||
required:
|
required:
|
||||||
- file
|
- file
|
||||||
- purpose
|
|
||||||
- expires_after_anchor
|
|
||||||
- expires_after_seconds
|
|
||||||
required: true
|
required: true
|
||||||
/v1/openai/v1/files:
|
/v1/openai/v1/files:
|
||||||
get:
|
get:
|
||||||
|
@ -4504,8 +4489,6 @@ paths:
|
||||||
- purpose: The intended purpose of the uploaded file.
|
- purpose: The intended purpose of the uploaded file.
|
||||||
|
|
||||||
- expires_after: Optional form values describing expiration for the 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: []
|
parameters: []
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
|
@ -4516,21 +4499,8 @@ paths:
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
purpose:
|
|
||||||
$ref: '#/components/schemas/OpenAIFilePurpose'
|
|
||||||
expires_after_anchor:
|
|
||||||
oneOf:
|
|
||||||
- type: string
|
|
||||||
- type: 'null'
|
|
||||||
expires_after_seconds:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: 'null'
|
|
||||||
required:
|
required:
|
||||||
- file
|
- file
|
||||||
- purpose
|
|
||||||
- expires_after_anchor
|
|
||||||
- expires_after_seconds
|
|
||||||
required: true
|
required: true
|
||||||
/v1/openai/v1/models:
|
/v1/openai/v1/models:
|
||||||
get:
|
get:
|
||||||
|
|
|
@ -110,9 +110,8 @@ class Files(Protocol):
|
||||||
async def openai_upload_file(
|
async def openai_upload_file(
|
||||||
self,
|
self,
|
||||||
file: Annotated[UploadFile, File()],
|
file: Annotated[UploadFile, File()],
|
||||||
purpose: Annotated[OpenAIFilePurpose, Form()],
|
purpose: OpenAIFilePurpose,
|
||||||
expires_after_anchor: Annotated[str | None, Form(alias="expires_after[anchor]")] = None,
|
expires_after: ExpiresAfter | None = None,
|
||||||
expires_after_seconds: Annotated[int | None, Form(alias="expires_after[seconds]")] = None,
|
|
||||||
# TODO: expires_after is producing strange openapi spec, params are showing up as a required w/ oneOf being null
|
# TODO: expires_after is producing strange openapi spec, params are showing up as a required w/ oneOf being null
|
||||||
) -> OpenAIFileObject:
|
) -> OpenAIFileObject:
|
||||||
"""
|
"""
|
||||||
|
@ -121,10 +120,11 @@ class Files(Protocol):
|
||||||
The file upload should be a multipart form request with:
|
The file upload should be a multipart form request with:
|
||||||
- file: The File object (not file name) to be uploaded.
|
- file: The File object (not file name) to be uploaded.
|
||||||
- purpose: The intended purpose of the uploaded file.
|
- 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 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 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.
|
:returns: An OpenAIFileObject representing the uploaded file.
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
|
@ -14,6 +14,7 @@ from fastapi import File, Form, Response, UploadFile
|
||||||
from llama_stack.apis.common.errors import ResourceNotFoundError
|
from llama_stack.apis.common.errors import ResourceNotFoundError
|
||||||
from llama_stack.apis.common.responses import Order
|
from llama_stack.apis.common.responses import Order
|
||||||
from llama_stack.apis.files import (
|
from llama_stack.apis.files import (
|
||||||
|
ExpiresAfter,
|
||||||
Files,
|
Files,
|
||||||
ListOpenAIFileResponse,
|
ListOpenAIFileResponse,
|
||||||
OpenAIFileDeleteResponse,
|
OpenAIFileDeleteResponse,
|
||||||
|
@ -86,14 +87,13 @@ class LocalfsFilesImpl(Files):
|
||||||
self,
|
self,
|
||||||
file: Annotated[UploadFile, File()],
|
file: Annotated[UploadFile, File()],
|
||||||
purpose: Annotated[OpenAIFilePurpose, Form()],
|
purpose: Annotated[OpenAIFilePurpose, Form()],
|
||||||
expires_after_anchor: Annotated[str | None, Form(alias="expires_after[anchor]")] = None,
|
expires_after: Annotated[ExpiresAfter | None, Form()] = None,
|
||||||
expires_after_seconds: Annotated[int | None, Form(alias="expires_after[seconds]")] = None,
|
|
||||||
) -> OpenAIFileObject:
|
) -> OpenAIFileObject:
|
||||||
"""Upload a file that can be used across various endpoints."""
|
"""Upload a file that can be used across various endpoints."""
|
||||||
if not self.sql_store:
|
if not self.sql_store:
|
||||||
raise RuntimeError("Files provider not initialized")
|
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")
|
raise NotImplementedError("File expiration is not supported by this provider")
|
||||||
|
|
||||||
file_id = self._generate_file_id()
|
file_id = self._generate_file_id()
|
||||||
|
|
|
@ -195,8 +195,7 @@ class S3FilesImpl(Files):
|
||||||
self,
|
self,
|
||||||
file: Annotated[UploadFile, File()],
|
file: Annotated[UploadFile, File()],
|
||||||
purpose: Annotated[OpenAIFilePurpose, Form()],
|
purpose: Annotated[OpenAIFilePurpose, Form()],
|
||||||
expires_after_anchor: Annotated[str | None, Form(alias="expires_after[anchor]")] = None,
|
expires_after: Annotated[ExpiresAfter | None, Form()] = None,
|
||||||
expires_after_seconds: Annotated[int | None, Form(alias="expires_after[seconds]")] = None,
|
|
||||||
) -> OpenAIFileObject:
|
) -> OpenAIFileObject:
|
||||||
file_id = f"file-{uuid.uuid4().hex}"
|
file_id = f"file-{uuid.uuid4().hex}"
|
||||||
|
|
||||||
|
@ -204,14 +203,6 @@ class S3FilesImpl(Files):
|
||||||
|
|
||||||
created_at = self._now()
|
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.
|
# the default is no expiration.
|
||||||
# to implement no expiration we set an expiration beyond the max.
|
# to implement no expiration we set an expiration beyond the max.
|
||||||
# we'll hide this fact from users when returning the file object.
|
# we'll hide this fact from users when returning the file object.
|
||||||
|
|
|
@ -228,12 +228,13 @@ class TestS3FilesImpl:
|
||||||
|
|
||||||
mock_now.return_value = 0
|
mock_now.return_value = 0
|
||||||
|
|
||||||
|
from llama_stack.apis.files import ExpiresAfter
|
||||||
|
|
||||||
sample_text_file.filename = "test_expired_file"
|
sample_text_file.filename = "test_expired_file"
|
||||||
uploaded = await s3_provider.openai_upload_file(
|
uploaded = await s3_provider.openai_upload_file(
|
||||||
file=sample_text_file,
|
file=sample_text_file,
|
||||||
purpose=OpenAIFilePurpose.ASSISTANTS,
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
||||||
expires_after_anchor="created_at",
|
expires_after=ExpiresAfter(anchor="created_at", seconds=two_hours),
|
||||||
expires_after_seconds=two_hours,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_now.return_value = two_hours * 2 # fast forward 4 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):
|
async def test_unsupported_expires_after_anchor(self, s3_provider, sample_text_file):
|
||||||
"""Unsupported anchor value should raise ValueError."""
|
"""Unsupported anchor value should raise ValueError."""
|
||||||
|
from llama_stack.apis.files import ExpiresAfter
|
||||||
|
|
||||||
sample_text_file.filename = "test_unsupported_expires_after_anchor"
|
sample_text_file.filename = "test_unsupported_expires_after_anchor"
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Input should be 'created_at'"):
|
with pytest.raises(ValueError, match="Input should be 'created_at'"):
|
||||||
await s3_provider.openai_upload_file(
|
await s3_provider.openai_upload_file(
|
||||||
file=sample_text_file,
|
file=sample_text_file,
|
||||||
purpose=OpenAIFilePurpose.ASSISTANTS,
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
||||||
expires_after_anchor="now",
|
expires_after=ExpiresAfter(anchor="now", seconds=3600), # type: ignore
|
||||||
expires_after_seconds=3600,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test_nonint_expires_after_seconds(self, s3_provider, sample_text_file):
|
async def test_nonint_expires_after_seconds(self, s3_provider, sample_text_file):
|
||||||
"""Non-integer seconds in expires_after should raise ValueError."""
|
"""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"
|
sample_text_file.filename = "test_nonint_expires_after_seconds"
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="should be a valid integer"):
|
with pytest.raises(ValueError, match="should be a valid integer"):
|
||||||
await s3_provider.openai_upload_file(
|
await s3_provider.openai_upload_file(
|
||||||
file=sample_text_file,
|
file=sample_text_file,
|
||||||
purpose=OpenAIFilePurpose.ASSISTANTS,
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
||||||
expires_after_anchor="created_at",
|
expires_after=ExpiresAfter(anchor="created_at", seconds="many"), # type: ignore
|
||||||
expires_after_seconds="many",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test_expires_after_seconds_out_of_bounds(self, s3_provider, sample_text_file):
|
async def test_expires_after_seconds_out_of_bounds(self, s3_provider, sample_text_file):
|
||||||
"""Seconds outside allowed range should raise ValueError."""
|
"""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"):
|
with pytest.raises(ValueError, match="greater than or equal to 3600"):
|
||||||
await s3_provider.openai_upload_file(
|
await s3_provider.openai_upload_file(
|
||||||
file=sample_text_file,
|
file=sample_text_file,
|
||||||
purpose=OpenAIFilePurpose.ASSISTANTS,
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
||||||
expires_after_anchor="created_at",
|
expires_after=ExpiresAfter(anchor="created_at", seconds=3599),
|
||||||
expires_after_seconds=3599,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="less than or equal to 2592000"):
|
with pytest.raises(ValueError, match="less than or equal to 2592000"):
|
||||||
await s3_provider.openai_upload_file(
|
await s3_provider.openai_upload_file(
|
||||||
file=sample_text_file,
|
file=sample_text_file,
|
||||||
purpose=OpenAIFilePurpose.ASSISTANTS,
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
||||||
expires_after_anchor="created_at",
|
expires_after=ExpiresAfter(anchor="created_at", seconds=2592001),
|
||||||
expires_after_seconds=2592001,
|
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue