mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(files): keep the SDK listing a list and build the OpenAI page at the proxy route
This commit is contained in:
parent
91391c1360
commit
5f4d667365
4 changed files with 82 additions and 27 deletions
|
|
@ -59,7 +59,6 @@ from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
|
|||
from litellm.llms.base_llm.image_generation.transformation import (
|
||||
BaseImageGenerationConfig,
|
||||
)
|
||||
from litellm.llms.base_llm.managed_resources.isolation import build_list_page
|
||||
from litellm.llms.base_llm.ocr.transformation import BaseOCRConfig, OCRResponse
|
||||
from litellm.llms.base_llm.realtime.http_transformation import BaseRealtimeHTTPConfig
|
||||
from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig
|
||||
|
|
@ -121,7 +120,6 @@ from litellm.types.llms.openai import (
|
|||
CreateBatchRequest,
|
||||
CreateFileRequest,
|
||||
FileContentRequest,
|
||||
FileListPage,
|
||||
HttpxBinaryResponseContent,
|
||||
OpenAIFileObject,
|
||||
ResponseInputParam,
|
||||
|
|
@ -4901,7 +4899,7 @@ class BaseLLMHTTPHandler:
|
|||
_is_async: bool = False,
|
||||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> FileListPage | Coroutine[object, object, FileListPage]:
|
||||
) -> list[OpenAIFileObject] | Coroutine[object, object, list[OpenAIFileObject]]:
|
||||
"""
|
||||
List all files
|
||||
"""
|
||||
|
|
@ -4956,10 +4954,9 @@ class BaseLLMHTTPHandler:
|
|||
files_per_page: Final = self._files_per_listing_page(
|
||||
response, provider_config, logging_obj, litellm_params, headers, sync_httpx_client, timeout
|
||||
)
|
||||
listed_files: Final = [ # mutable-ok: build_list_page takes the list the files contract returns
|
||||
return [ # mutable-ok: the files contract returns the listing as a list
|
||||
listed_file for page_files in files_per_page for listed_file in page_files
|
||||
]
|
||||
return FileListPage(**build_list_page(listed_files))
|
||||
|
||||
async def async_list_files(
|
||||
self,
|
||||
|
|
@ -4970,7 +4967,7 @@ class BaseLLMHTTPHandler:
|
|||
logging_obj: LiteLLMLoggingObj,
|
||||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> FileListPage:
|
||||
) -> list[OpenAIFileObject]:
|
||||
"""
|
||||
Async list all files
|
||||
"""
|
||||
|
|
@ -5014,10 +5011,9 @@ class BaseLLMHTTPHandler:
|
|||
files_per_page: Final = self._files_per_async_listing_page(
|
||||
response, provider_config, logging_obj, litellm_params, headers, async_httpx_client, timeout
|
||||
)
|
||||
listed_files: Final = [ # mutable-ok: build_list_page takes the list the files contract returns
|
||||
return [ # mutable-ok: the files contract returns the listing as a list
|
||||
listed_file async for page_files in files_per_page for listed_file in page_files
|
||||
]
|
||||
return FileListPage(**build_list_page(listed_files))
|
||||
|
||||
def _files_per_listing_page(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from litellm.litellm_core_utils.cloud_storage_security import (
|
|||
)
|
||||
from litellm.litellm_core_utils.core_helpers import get_or_create_metadata_bucket
|
||||
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
|
||||
from litellm.llms.base_llm.managed_resources.isolation import build_list_page
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
||||
|
|
@ -85,6 +86,7 @@ from litellm.router import Router
|
|||
from litellm.types.llms.openai import (
|
||||
CREATE_FILE_REQUESTS_PURPOSE,
|
||||
FileExpiresAfter,
|
||||
FileListPage,
|
||||
OpenAIFileObject,
|
||||
OpenAIFilesPurpose,
|
||||
)
|
||||
|
|
@ -92,6 +94,7 @@ from litellm.types.llms.openai import (
|
|||
router: Final = APIRouter()
|
||||
|
||||
_MAX_BATCH_FILE_SIZE_MB_ADAPTER: Final = TypeAdapter(int | None)
|
||||
_LISTED_FILES_ADAPTER: Final = TypeAdapter(list[OpenAIFileObject])
|
||||
|
||||
|
||||
class UploadedFileInfo(TypedDict):
|
||||
|
|
@ -1441,6 +1444,12 @@ async def delete_file(
|
|||
)
|
||||
|
||||
|
||||
def _as_file_list_page(response: object) -> object:
|
||||
if not isinstance(response, list):
|
||||
return response
|
||||
return FileListPage(**build_list_page(_LISTED_FILES_ADAPTER.validate_python(response)))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{provider}/v1/files",
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
|
|
@ -1587,6 +1596,7 @@ async def list_files(
|
|||
status_code=500,
|
||||
detail="Either 'provider' or 'target_model_names' must be provided e.g. `?target_model_names=gpt-4o`",
|
||||
)
|
||||
response = _as_file_list_page(response) # rebind-ok: each dispatch branch above binds response
|
||||
|
||||
## POST CALL HOOKS ###
|
||||
_response: Final = await proxy_logging_obj.post_call_success_hook(
|
||||
|
|
|
|||
|
|
@ -3024,20 +3024,13 @@ class TestBedrockFileListTransformation:
|
|||
return_value=httpx.Response(200, content=self.LISTING)
|
||||
)
|
||||
|
||||
page = litellm.file_list(custom_llm_provider="bedrock", purpose="batch", **_bedrock_s3_params())
|
||||
files = litellm.file_list(custom_llm_provider="bedrock", purpose="batch", **_bedrock_s3_params())
|
||||
|
||||
assert route.called
|
||||
request = route.calls[0].request
|
||||
assert request.headers["Authorization"].startswith("AWS4-HMAC-SHA256")
|
||||
assert _sent_signature(request.headers) == _s3_signature_for("GET", str(request.url), request.headers)
|
||||
assert [file.id for file in page.data] == list(self.BATCH_IDS)
|
||||
assert (page.object, page.first_id, page.last_id, page.has_more) == (
|
||||
"list",
|
||||
self.BATCH_IDS[0],
|
||||
self.BATCH_IDS[-1],
|
||||
False,
|
||||
)
|
||||
assert page.model_dump()["object"] == "list"
|
||||
assert [file.id for file in files] == list(self.BATCH_IDS)
|
||||
|
||||
def test_file_list_uses_trusted_snapshot_bucket_without_env(self, monkeypatch):
|
||||
import httpx
|
||||
|
|
@ -3058,7 +3051,7 @@ class TestBedrockFileListTransformation:
|
|||
)
|
||||
|
||||
assert route.called
|
||||
assert [file.id for file in files.data] == [*self.BATCH_IDS, self.OUTPUT_ID]
|
||||
assert [file.id for file in files] == [*self.BATCH_IDS, self.OUTPUT_ID]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_end_to_end_sends_signed_listing(self, monkeypatch):
|
||||
|
|
@ -3083,8 +3076,7 @@ class TestBedrockFileListTransformation:
|
|||
assert route.called
|
||||
request = route.calls[0].request
|
||||
assert _sent_signature(request.headers) == _s3_signature_for("GET", str(request.url), request.headers)
|
||||
assert [file.id for file in files.data] == [self.OUTPUT_ID]
|
||||
assert (files.object, files.first_id, files.last_id, files.has_more) == ("list", self.OUTPUT_ID, self.OUTPUT_ID, False)
|
||||
assert [file.id for file in files] == [self.OUTPUT_ID]
|
||||
|
||||
def test_transform_list_files_request_narrows_prefix_to_requested_purpose(self, monkeypatch):
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -3185,11 +3177,11 @@ class TestBedrockFileListTransformation:
|
|||
return_value=httpx.Response(200, content=self.EMPTY_LISTING)
|
||||
)
|
||||
|
||||
page = litellm.file_list(custom_llm_provider="bedrock", purpose="user_data", **_bedrock_s3_params())
|
||||
files = litellm.file_list(custom_llm_provider="bedrock", purpose="user_data", **_bedrock_s3_params())
|
||||
|
||||
assert route.call_count == 1
|
||||
assert "prefix" not in route.calls[0].request.url.params
|
||||
assert (page.data, page.has_more) == ([], False)
|
||||
assert files == []
|
||||
|
||||
def test_transform_list_files_request_lists_the_output_bucket_without_an_input_bucket(self, monkeypatch):
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -3253,7 +3245,7 @@ class TestBedrockFileListTransformation:
|
|||
assert route.called
|
||||
request = route.calls[0].request
|
||||
assert _sent_signature(request.headers) == _s3_signature_for("GET", str(request.url), request.headers)
|
||||
assert [file.id for file in files.data] == [self.OUTPUT_BUCKET_ID]
|
||||
assert [file.id for file in files] == [self.OUTPUT_BUCKET_ID]
|
||||
|
||||
def test_transform_list_files_next_request_signs_the_continuation_page(self, monkeypatch):
|
||||
import httpx
|
||||
|
|
@ -3350,7 +3342,7 @@ class TestBedrockFileListTransformation:
|
|||
**_trusted_bucket_snapshot(s3_bucket_name="my-bucket"),
|
||||
)
|
||||
|
||||
self._assert_paged_listing(first_page, last_page, files.data)
|
||||
self._assert_paged_listing(first_page, last_page, files)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_follows_continuation_tokens_across_pages(self, monkeypatch):
|
||||
|
|
@ -3372,7 +3364,7 @@ class TestBedrockFileListTransformation:
|
|||
**_trusted_bucket_snapshot(s3_bucket_name="my-bucket"),
|
||||
)
|
||||
|
||||
self._assert_paged_listing(first_page, last_page, files.data)
|
||||
self._assert_paged_listing(first_page, last_page, files)
|
||||
|
||||
OVERSIZED_PAGE_SIZE = 3000
|
||||
OVERSIZED_PAGE_COUNT = 6
|
||||
|
|
@ -3429,7 +3421,7 @@ class TestBedrockFileListTransformation:
|
|||
**_trusted_bucket_snapshot(s3_bucket_name="my-bucket"),
|
||||
)
|
||||
|
||||
self._assert_capped_listing(route, files.data)
|
||||
self._assert_capped_listing(route, files)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_stops_at_the_openai_listing_ceiling(self, monkeypatch):
|
||||
|
|
@ -3450,7 +3442,7 @@ class TestBedrockFileListTransformation:
|
|||
**_trusted_bucket_snapshot(s3_bucket_name="my-bucket"),
|
||||
)
|
||||
|
||||
self._assert_capped_listing(route, files.data)
|
||||
self._assert_capped_listing(route, files)
|
||||
|
||||
def test_file_list_end_to_end_surfaces_the_s3_error_body(self, monkeypatch):
|
||||
import httpx
|
||||
|
|
|
|||
|
|
@ -2441,6 +2441,63 @@ def test_list_files_resolves_wildcard_deployment_credentials(
|
|||
proxy_logging_obj.post_call_failure_hook.assert_not_called()
|
||||
|
||||
|
||||
def test_list_files_by_model_returns_an_openai_page_for_a_provider_listing(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
from litellm.types.llms.openai import FileListPage
|
||||
|
||||
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, llm_router)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
|
||||
proxy_logging_obj.update_request_status = mocker.AsyncMock()
|
||||
proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=None)
|
||||
proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock()
|
||||
|
||||
listed_files = [
|
||||
OpenAIFileObject(
|
||||
id=f"file-{index}",
|
||||
bytes=index,
|
||||
created_at=index,
|
||||
filename=f"{index}.jsonl",
|
||||
object="file",
|
||||
purpose="batch",
|
||||
status="uploaded",
|
||||
)
|
||||
for index in (1, 2)
|
||||
]
|
||||
|
||||
async def _mock_afile_list(**kwargs):
|
||||
return list(listed_files)
|
||||
|
||||
monkeypatch.setattr(litellm, "afile_list", _mock_afile_list)
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
api_key="test-key",
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN,
|
||||
user_id="test-user",
|
||||
)
|
||||
|
||||
try:
|
||||
response = client.get(
|
||||
"/v1/files?target_model_names=gpt-3.5-turbo",
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["object"] == "list"
|
||||
assert [listed["id"] for listed in body["data"]] == ["file-1", "file-2"]
|
||||
assert (body["first_id"], body["last_id"], body["has_more"]) == ("file-1", "file-2", False)
|
||||
hook_response = proxy_logging_obj.post_call_success_hook.call_args.kwargs["response"]
|
||||
assert isinstance(hook_response, FileListPage)
|
||||
assert [listed.id for listed in hook_response.data] == ["file-1", "file-2"]
|
||||
|
||||
|
||||
def test_list_files_model_routing_does_not_forward_custom_llm_provider_twice(
|
||||
mocker: MockerFixture, monkeypatch, llm_router: Router
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue