mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(files): keep the list cursor usable on a filtered page
A page whose rows are all dropped by the purpose filter, or by a row that does not parse, used to come back with an empty data list, has_more true and last_id null, so the caller had no cursor to advance with and stopped one page short of files it owns. last_id now falls back to the last row the page read. Also drops the OpenAIFilesPurpose import that the widened purpose annotation left unused.
This commit is contained in:
parent
138b0da21f
commit
a15b81d3d7
3 changed files with 56 additions and 7 deletions
|
|
@ -67,7 +67,6 @@ from litellm.types.llms.openai import ( # pyright: ignore[reportAttributeAccess
|
|||
CreateFileRequest,
|
||||
FileObject,
|
||||
OpenAIFileObject,
|
||||
OpenAIFilesPurpose,
|
||||
ResponsesAPIResponse,
|
||||
)
|
||||
from litellm.types.utils import (
|
||||
|
|
@ -1386,7 +1385,10 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
Pagination is keyset based on ``unified_file_id`` so a key that owns
|
||||
every file on the proxy still reads one bounded page at a time.
|
||||
``purpose`` is applied after parsing because the managed file table
|
||||
keeps it inside the ``file_object`` blob instead of a column.
|
||||
keeps it inside the ``file_object`` blob instead of a column, so a
|
||||
narrowed page can hold fewer files than ``limit``. ``last_id`` then
|
||||
falls back to the last row the page read, which keeps the cursor
|
||||
usable even when every file on the page was filtered out.
|
||||
"""
|
||||
validate_file_list_limit(limit)
|
||||
if limit == 0:
|
||||
|
|
@ -1416,14 +1418,19 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
**cursor_args,
|
||||
)
|
||||
has_more: Final = len(rows) > page_size
|
||||
page_rows: Final = rows[:page_size]
|
||||
|
||||
files: Final = [
|
||||
parsed_file_object.model_copy(update={"id": row.unified_file_id})
|
||||
for row in rows[:page_size]
|
||||
for row in page_rows
|
||||
if (parsed_file_object := _parse_managed_file_object(row.file_object, row.unified_file_id)) is not None
|
||||
and (purpose is None or parsed_file_object.purpose == purpose)
|
||||
]
|
||||
return build_list_page(files, has_more=has_more)
|
||||
return build_list_page(
|
||||
files,
|
||||
has_more=has_more,
|
||||
next_cursor_id=page_rows[-1].unified_file_id if page_rows else None,
|
||||
)
|
||||
|
||||
def _is_batch_polling_enabled(self) -> bool:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -19,15 +19,23 @@ from litellm.proxy._types import (
|
|||
)
|
||||
|
||||
|
||||
def build_list_page(items: list[Any], has_more: bool = False) -> dict[str, Any]:
|
||||
def build_list_page(
|
||||
items: list[Any],
|
||||
has_more: bool = False,
|
||||
next_cursor_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the OpenAI-style paginated list response shape used by managed
|
||||
file/batch/vector-store listings. ``first_id`` and ``last_id`` are
|
||||
sourced from each item's ``.id`` attribute."""
|
||||
sourced from each item's ``.id`` attribute.
|
||||
|
||||
A listing that filters rows out after reading them can pass
|
||||
``next_cursor_id`` so an empty page still carries the cursor the caller
|
||||
needs to reach the rows behind it."""
|
||||
return {
|
||||
"object": "list",
|
||||
"data": items,
|
||||
"first_id": items[0].id if items else None,
|
||||
"last_id": items[-1].id if items else None,
|
||||
"last_id": items[-1].id if items else next_cursor_id,
|
||||
"has_more": has_more,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -334,6 +334,40 @@ async def test_afile_list_filters_by_purpose():
|
|||
assert [file.id for file in response["data"]] == ["unified-batch"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_keeps_a_usable_cursor_when_a_page_filters_everything_out():
|
||||
managed_files, _ = _make_managed_files_over_rows(
|
||||
[
|
||||
_make_managed_file_row("unified-0"),
|
||||
_make_managed_file_row("unified-1"),
|
||||
_make_managed_file_row("unified-2", purpose="batch"),
|
||||
]
|
||||
)
|
||||
user_api_key_dict = _make_user_api_key_dict()
|
||||
|
||||
first_page = await managed_files.afile_list(
|
||||
purpose="batch",
|
||||
litellm_parent_otel_span=None,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
limit=2,
|
||||
)
|
||||
|
||||
assert first_page["data"] == []
|
||||
assert first_page["has_more"] is True
|
||||
assert first_page["last_id"] == "unified-1"
|
||||
|
||||
second_page = await managed_files.afile_list(
|
||||
purpose="batch",
|
||||
litellm_parent_otel_span=None,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
limit=2,
|
||||
after=first_page["last_id"],
|
||||
)
|
||||
|
||||
assert [file.id for file in second_page["data"]] == ["unified-2"]
|
||||
assert second_page["has_more"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_honors_limit_and_reports_more_pages():
|
||||
managed_files, table = _make_managed_files_over_rows(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue