mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-18 03:31:23 +00:00
Add support for expires after param
This commit is contained in:
+19
-7
@@ -27,6 +27,7 @@ from litellm.llms.vertex_ai.files.handler import VertexAIFilesHandler
|
||||
from litellm.types.llms.openai import (
|
||||
CreateFileRequest,
|
||||
FileContentRequest,
|
||||
FileExpiresAfter,
|
||||
FileTypes,
|
||||
HttpxBinaryResponseContent,
|
||||
OpenAIFileObject,
|
||||
@@ -58,6 +59,7 @@ anthropic_files_instance = AnthropicFilesHandler()
|
||||
async def acreate_file(
|
||||
file: FileTypes,
|
||||
purpose: Literal["assistants", "batch", "fine-tune"],
|
||||
expires_after: Optional[FileExpiresAfter] = None,
|
||||
custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai",
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
extra_body: Optional[Dict[str, str]] = None,
|
||||
@@ -75,6 +77,7 @@ async def acreate_file(
|
||||
call_args = {
|
||||
"file": file,
|
||||
"purpose": purpose,
|
||||
"expires_after": expires_after,
|
||||
"custom_llm_provider": custom_llm_provider,
|
||||
"extra_headers": extra_headers,
|
||||
"extra_body": extra_body,
|
||||
@@ -83,7 +86,6 @@ async def acreate_file(
|
||||
|
||||
# Use a partial function to pass your keyword arguments
|
||||
func = partial(create_file, **call_args)
|
||||
|
||||
# Add the context to the function
|
||||
ctx = contextvars.copy_context()
|
||||
func_with_context = partial(ctx.run, func)
|
||||
@@ -102,6 +104,7 @@ async def acreate_file(
|
||||
def create_file(
|
||||
file: FileTypes,
|
||||
purpose: Literal["assistants", "batch", "fine-tune"],
|
||||
expires_after: Optional[FileExpiresAfter] = None,
|
||||
custom_llm_provider: Optional[Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"]] = None,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
extra_body: Optional[Dict[str, str]] = None,
|
||||
@@ -141,12 +144,21 @@ def create_file(
|
||||
elif timeout is None:
|
||||
timeout = 600.0
|
||||
|
||||
_create_file_request = CreateFileRequest(
|
||||
file=file,
|
||||
purpose=purpose,
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
)
|
||||
if expires_after is not None:
|
||||
_create_file_request = CreateFileRequest(
|
||||
file=file,
|
||||
purpose=purpose,
|
||||
expires_after=expires_after,
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
)
|
||||
else:
|
||||
_create_file_request = CreateFileRequest(
|
||||
file=file,
|
||||
purpose=purpose,
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
)
|
||||
|
||||
provider_config = ProviderConfigManager.get_provider_files_config(
|
||||
model="",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import time
|
||||
import types
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
@@ -10,7 +11,6 @@ from typing import (
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -20,6 +20,7 @@ import httpx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiohttp import ClientSession
|
||||
|
||||
import openai
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
from openai.types.beta.assistant_deleted import AssistantDeleted
|
||||
@@ -1549,7 +1550,7 @@ class OpenAIFilesAPI(BaseLLM):
|
||||
create_file_data: CreateFileRequest,
|
||||
openai_client: AsyncOpenAI,
|
||||
) -> OpenAIFileObject:
|
||||
response = await openai_client.files.create(**create_file_data)
|
||||
response = await openai_client.files.create(**create_file_data) # type: ignore[arg-type]
|
||||
return OpenAIFileObject(**response.model_dump())
|
||||
|
||||
def create_file(
|
||||
@@ -1585,7 +1586,7 @@ class OpenAIFilesAPI(BaseLLM):
|
||||
return self.acreate_file( # type: ignore
|
||||
create_file_data=create_file_data, openai_client=openai_client
|
||||
)
|
||||
response = cast(OpenAI, openai_client).files.create(**create_file_data)
|
||||
response = cast(OpenAI, openai_client).files.create(**create_file_data) # type: ignore[arg-type]
|
||||
return OpenAIFileObject(**response.model_dump())
|
||||
|
||||
async def afile_content(
|
||||
|
||||
@@ -39,6 +39,7 @@ from litellm.proxy.utils import ProxyLogging, is_known_model
|
||||
from litellm.router import Router
|
||||
from litellm.types.llms.openai import (
|
||||
CREATE_FILE_REQUESTS_PURPOSE,
|
||||
FileExpiresAfter,
|
||||
OpenAIFileObject,
|
||||
OpenAIFilesPurpose,
|
||||
)
|
||||
@@ -272,7 +273,8 @@ async def create_file(
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-F purpose="batch" \
|
||||
-F file="@mydata.jsonl"
|
||||
|
||||
-F expires_after[anchor]="created_at" \
|
||||
-F expires_after[seconds]=2592000
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
@@ -329,6 +331,25 @@ async def create_file(
|
||||
if litellm_metadata is not None:
|
||||
data["litellm_metadata"] = litellm_metadata
|
||||
|
||||
# Parse expires_after if provided
|
||||
expires_after = None
|
||||
form_data = await request.form()
|
||||
expires_after_anchor = form_data.get("expires_after[anchor]")
|
||||
expires_after_seconds_str = form_data.get("expires_after[seconds]")
|
||||
|
||||
if expires_after_anchor is not None or expires_after_seconds_str is not None:
|
||||
if expires_after_anchor is None or expires_after_seconds_str is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Both expires_after[anchor] and expires_after[seconds] must be provided if expires_after is specified",
|
||||
},
|
||||
)
|
||||
expires_after = FileExpiresAfter(
|
||||
anchor=expires_after_anchor,
|
||||
seconds=int(expires_after_seconds_str),
|
||||
)
|
||||
|
||||
# Include original request and headers in the data
|
||||
data = await add_litellm_data_to_request(
|
||||
data=data,
|
||||
@@ -354,7 +375,10 @@ async def create_file(
|
||||
)
|
||||
|
||||
_create_file_request = CreateFileRequest(
|
||||
file=file_data, purpose=cast(CREATE_FILE_REQUESTS_PURPOSE, purpose), **data
|
||||
file=file_data,
|
||||
purpose=cast(CREATE_FILE_REQUESTS_PURPOSE, purpose),
|
||||
expires_after=expires_after,
|
||||
**data
|
||||
)
|
||||
|
||||
response = await route_create_file(
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
)
|
||||
|
||||
import httpx
|
||||
from openai import Omit
|
||||
from openai._legacy_response import (
|
||||
HttpxBinaryResponseContent as _HttpxBinaryResponseContent,
|
||||
)
|
||||
@@ -69,7 +70,7 @@ from openai.types.responses.response_create_params import (
|
||||
ToolParam,
|
||||
)
|
||||
from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
|
||||
from pydantic import BaseModel, ConfigDict, Discriminator, Field, PrivateAttr
|
||||
from pydantic import BaseModel, ConfigDict, Discriminator, PrivateAttr
|
||||
from typing_extensions import Annotated, Dict, Required, TypedDict, override
|
||||
|
||||
from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject
|
||||
@@ -346,6 +347,19 @@ class OpenAIFileObject(BaseModel):
|
||||
CREATE_FILE_REQUESTS_PURPOSE = Literal["assistants", "batch", "fine-tune"]
|
||||
|
||||
|
||||
# File expiration policy
|
||||
class FileExpiresAfter(TypedDict):
|
||||
"""
|
||||
File expiration policy
|
||||
|
||||
Properties:
|
||||
anchor: Anchor timestamp after which the expiration policy applies. Supported anchors: created_at.
|
||||
seconds: The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days).
|
||||
"""
|
||||
anchor: Required[Literal["created_at"]]
|
||||
seconds: Required[int]
|
||||
|
||||
|
||||
# OpenAI Files Types
|
||||
class CreateFileRequest(TypedDict, total=False):
|
||||
"""
|
||||
@@ -357,6 +371,7 @@ class CreateFileRequest(TypedDict, total=False):
|
||||
purpose: Literal['assistants', 'batch', 'fine-tune']
|
||||
|
||||
Optional Params:
|
||||
expires_after: Optional[FileExpiresAfter] - The expiration policy for a file
|
||||
extra_headers: Optional[Dict[str, str]]
|
||||
extra_body: Optional[Dict[str, str]] = None
|
||||
timeout: Optional[float] = None
|
||||
@@ -364,6 +379,7 @@ class CreateFileRequest(TypedDict, total=False):
|
||||
|
||||
file: Required[FileTypes]
|
||||
purpose: Required[CREATE_FILE_REQUESTS_PURPOSE]
|
||||
expires_after: Optional[FileExpiresAfter]
|
||||
extra_headers: Optional[Dict[str, str]]
|
||||
extra_body: Optional[Dict[str, str]]
|
||||
timeout: Optional[float]
|
||||
|
||||
@@ -477,3 +477,290 @@ def test_create_file_for_each_model(
|
||||
openai_call_found = True
|
||||
break
|
||||
assert openai_call_found, "OpenAI call not found with expected parameters"
|
||||
|
||||
|
||||
def test_create_file_with_expires_after(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
"""
|
||||
Test that expires_after is properly parsed and passed through when creating a file
|
||||
"""
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
# Verify expires_after is in the request
|
||||
if isinstance(create_file_request, dict):
|
||||
expires_after = create_file_request.get("expires_after")
|
||||
else:
|
||||
expires_after = getattr(create_file_request, "expires_after", None)
|
||||
|
||||
# Verify expires_after was passed correctly
|
||||
assert expires_after is not None, "expires_after should be in the request"
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 2592000
|
||||
|
||||
# Return a dummy response
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
object="file",
|
||||
bytes=100,
|
||||
created_at=1234567890,
|
||||
filename="mydata.jsonl",
|
||||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
# Create test file content
|
||||
test_file_content = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
test_file = ("mydata.jsonl", test_file_content, "application/json")
|
||||
|
||||
# Test with expires_after
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
files={"file": test_file},
|
||||
data={
|
||||
"purpose": "fine-tune",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
"expires_after[anchor]": "created_at",
|
||||
"expires_after[seconds]": "2592000", # 30 days
|
||||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == "file-abc123"
|
||||
assert result["purpose"] == "fine-tune"
|
||||
|
||||
|
||||
def test_create_file_with_expires_after_missing_anchor(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
"""
|
||||
Test that an error is returned when expires_after[anchor] is missing
|
||||
"""
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
test_file_content = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
test_file = ("mydata.jsonl", test_file_content, "application/json")
|
||||
|
||||
# Test with only expires_after[seconds], missing anchor
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
files={"file": test_file},
|
||||
data={
|
||||
"purpose": "fine-tune",
|
||||
"expires_after[seconds]": "2592000",
|
||||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
error_detail = response.json()
|
||||
assert "expires_after" in error_detail["error"]["message"].lower() or "both" in error_detail["error"]["message"].lower()
|
||||
|
||||
|
||||
def test_create_file_with_expires_after_missing_seconds(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
"""
|
||||
Test that an error is returned when expires_after[seconds] is missing
|
||||
"""
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
test_file_content = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
test_file = ("mydata.jsonl", test_file_content, "application/json")
|
||||
|
||||
# Test with only expires_after[anchor], missing seconds
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
files={"file": test_file},
|
||||
data={
|
||||
"purpose": "fine-tune",
|
||||
"expires_after[anchor]": "created_at",
|
||||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
error_detail = response.json()
|
||||
assert "expires_after" in error_detail["error"]["message"].lower() or "both" in error_detail["error"]["message"].lower()
|
||||
|
||||
|
||||
def test_create_file_with_expires_after_valid_values(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
"""
|
||||
Test that expires_after works with valid anchor and seconds values
|
||||
"""
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
# Verify expires_after is in the request
|
||||
if isinstance(create_file_request, dict):
|
||||
expires_after = create_file_request.get("expires_after")
|
||||
else:
|
||||
expires_after = getattr(create_file_request, "expires_after", None)
|
||||
|
||||
# Verify expires_after was passed correctly
|
||||
assert expires_after is not None, "expires_after should be in the request"
|
||||
assert expires_after["anchor"] == "created_at"
|
||||
assert expires_after["seconds"] == 3600
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
object="file",
|
||||
bytes=100,
|
||||
created_at=1234567890,
|
||||
filename="mydata.jsonl",
|
||||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
test_file_content = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
test_file = ("mydata.jsonl", test_file_content, "application/json")
|
||||
|
||||
# Test with valid expires_after values
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
files={"file": test_file},
|
||||
data={
|
||||
"purpose": "fine-tune",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
"expires_after[anchor]": "created_at",
|
||||
"expires_after[seconds]": "3600", # Minimum valid value (1 hour)
|
||||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == "file-abc123"
|
||||
assert result["purpose"] == "fine-tune"
|
||||
|
||||
|
||||
def test_create_file_without_expires_after(mocker: MockerFixture, monkeypatch, llm_router: Router):
|
||||
"""
|
||||
Test that file creation works normally without expires_after
|
||||
"""
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
proxy_logging_obj = ProxyLogging(
|
||||
user_api_key_cache=DualCache(default_in_memory_ttl=1)
|
||||
)
|
||||
proxy_logging_obj._add_proxy_hooks(llm_router)
|
||||
|
||||
class DummyManagedFiles(BaseFileEndpoints):
|
||||
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
|
||||
# Verify expires_after is None when not provided
|
||||
if isinstance(create_file_request, dict):
|
||||
expires_after = create_file_request.get("expires_after")
|
||||
else:
|
||||
expires_after = getattr(create_file_request, "expires_after", None)
|
||||
|
||||
# expires_after should be None when not provided
|
||||
assert expires_after is None, "expires_after should be None when not provided"
|
||||
|
||||
return OpenAIFileObject(
|
||||
id="file-abc123",
|
||||
object="file",
|
||||
bytes=100,
|
||||
created_at=1234567890,
|
||||
filename="mydata.jsonl",
|
||||
purpose="fine-tune",
|
||||
status="uploaded",
|
||||
)
|
||||
|
||||
async def afile_retrieve(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_list(self, purpose, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_delete(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
async def afile_content(self, file_id, litellm_parent_otel_span):
|
||||
raise NotImplementedError("Not implemented for test")
|
||||
|
||||
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
|
||||
)
|
||||
|
||||
test_file_content = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
test_file = ("mydata.jsonl", test_file_content, "application/json")
|
||||
|
||||
# Test without expires_after
|
||||
response = client.post(
|
||||
"/v1/files",
|
||||
files={"file": test_file},
|
||||
data={
|
||||
"purpose": "fine-tune",
|
||||
"target_model_names": "gpt-3.5-turbo",
|
||||
},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == "file-abc123"
|
||||
assert result["purpose"] == "fine-tune"
|
||||
|
||||
Reference in New Issue
Block a user