mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(files): bound the queries a filtered file page can cost
The chunk loop read `limit + 1` rows at a time, so a small limit whose matches sit far behind the newest rows advanced a couple of rows per query. A `purpose` that matches only the last of 10000 owned rows at `limit=1` cost 5001 sequential find_many calls for one HTTP request, which any authenticated caller could ask for on purpose. Once a scan has to continue past its first chunk, widen the chunk to FILE_LIST_CONTINUATION_CHUNK_SIZE. That same case now costs 21 queries. The first chunk keeps its `limit + 1` size, so a page the newest rows already fill still costs exactly one query and reads nothing extra. Rows whose blob will not parse drop out of a page the way a filter does, so they get the bound too, not just the purpose filter. The floor only changes how many round trips a page costs, never what it returns: chunk boundaries do not affect a keyset scan, so the page is still `matches[:page_size]`, `has_more` is still `len(matches) > page_size`, and empty data still implies `has_more` false.
This commit is contained in:
parent
6b63623ca0
commit
b9f5c45aa8
3 changed files with 72 additions and 2 deletions
|
|
@ -45,6 +45,7 @@ from litellm.proxy._types import (
|
|||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
FILE_LIST_CONTINUATION_CHUNK_SIZE,
|
||||
MAX_FILE_LIST_LIMIT,
|
||||
_is_base64_encoded_unified_file_id,
|
||||
apply_unified_file_ids,
|
||||
|
|
@ -1390,7 +1391,10 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
yield fewer matches than the page holds. Successive chunks are read
|
||||
until the page is full or the caller's rows run out, which keeps
|
||||
``data`` non-empty while matches remain and its last id usable as the
|
||||
next cursor.
|
||||
next cursor. A first chunk that fills the page costs one query; once a
|
||||
scan has to continue past it, the chunk widens to
|
||||
``FILE_LIST_CONTINUATION_CHUNK_SIZE`` so a page whose matches sit far
|
||||
behind the newest rows cannot degenerate into thousands of queries.
|
||||
"""
|
||||
validate_file_list_limit(limit)
|
||||
|
||||
|
|
@ -1412,9 +1416,9 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
)
|
||||
|
||||
page_size: Final = min(limit or MAX_FILE_LIST_LIMIT, MAX_FILE_LIST_LIMIT)
|
||||
chunk_size: Final = page_size + 1
|
||||
matches: Final[List[OpenAIFileObject]] = []
|
||||
cursor_id = after
|
||||
chunk_size = page_size + 1
|
||||
|
||||
while len(matches) <= page_size:
|
||||
cursor_args: _CursorPageArgs = {"cursor": {"unified_file_id": cursor_id}, "skip": 1} if cursor_id else {}
|
||||
|
|
@ -1433,6 +1437,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
if len(chunk) < chunk_size:
|
||||
break
|
||||
cursor_id = chunk[-1].unified_file_id
|
||||
chunk_size = max(chunk_size, FILE_LIST_CONTINUATION_CHUNK_SIZE)
|
||||
|
||||
return build_list_page(matches[:page_size], has_more=len(matches) > page_size)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ if TYPE_CHECKING:
|
|||
|
||||
MAX_FILE_LIST_LIMIT: Final = 10000
|
||||
|
||||
FILE_LIST_CONTINUATION_CHUNK_SIZE: Final = 500
|
||||
|
||||
|
||||
def validate_file_list_limit(limit: int | None) -> None:
|
||||
"""Reject a ``limit`` outside the range OpenAI documents for GET /v1/files."""
|
||||
|
|
|
|||
|
|
@ -441,6 +441,69 @@ async def test_afile_list_fills_a_page_past_rows_that_do_not_parse():
|
|||
assert page["has_more"] is False
|
||||
|
||||
|
||||
_DEEP_SCAN_ROW_COUNT = 2000
|
||||
_DEEP_SCAN_QUERY_BUDGET = 10
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_bounds_the_queries_a_deep_purpose_match_costs():
|
||||
"""A tiny limit over rows the filter drops must not turn one request into thousands of queries."""
|
||||
managed_files, table = _make_managed_files_over_rows(
|
||||
[_make_managed_file_row(f"unified-{index:05d}") for index in range(_DEEP_SCAN_ROW_COUNT)]
|
||||
+ [_make_managed_file_row("unified-match", purpose="batch")]
|
||||
)
|
||||
|
||||
page = await managed_files.afile_list(
|
||||
purpose="batch",
|
||||
litellm_parent_otel_span=None,
|
||||
user_api_key_dict=_make_user_api_key_dict(),
|
||||
limit=1,
|
||||
)
|
||||
|
||||
assert [file.id for file in page["data"]] == ["unified-match"]
|
||||
assert page["has_more"] is False
|
||||
assert len(table.find_many_calls) <= _DEEP_SCAN_QUERY_BUDGET
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_bounds_the_queries_a_deep_unparseable_run_costs():
|
||||
"""Rows that will not parse drop out like a filter does, so they get the same bound."""
|
||||
managed_files, table = _make_managed_files_over_rows(
|
||||
[_make_unparseable_managed_file_row(f"unified-{index:05d}") for index in range(_DEEP_SCAN_ROW_COUNT)]
|
||||
+ [_make_managed_file_row("unified-parses")]
|
||||
)
|
||||
|
||||
page = await managed_files.afile_list(
|
||||
purpose=None,
|
||||
litellm_parent_otel_span=None,
|
||||
user_api_key_dict=_make_user_api_key_dict(),
|
||||
limit=1,
|
||||
)
|
||||
|
||||
assert [file.id for file in page["data"]] == ["unified-parses"]
|
||||
assert page["has_more"] is False
|
||||
assert len(table.find_many_calls) <= _DEEP_SCAN_QUERY_BUDGET
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_reads_one_chunk_when_the_first_one_fills_the_page():
|
||||
"""The widened chunk must stay off the common path, where the newest rows already fill the page."""
|
||||
managed_files, table = _make_managed_files_over_rows(
|
||||
[_make_managed_file_row(f"unified-{index:05d}") for index in range(_DEEP_SCAN_ROW_COUNT)]
|
||||
)
|
||||
|
||||
page = await managed_files.afile_list(
|
||||
purpose=None,
|
||||
litellm_parent_otel_span=None,
|
||||
user_api_key_dict=_make_user_api_key_dict(),
|
||||
limit=2,
|
||||
)
|
||||
|
||||
assert [file.id for file in page["data"]] == ["unified-00000", "unified-00001"]
|
||||
assert page["has_more"] is True
|
||||
assert [call["take"] for call in table.find_many_calls] == [3]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_list_reports_no_more_pages_when_nothing_matches():
|
||||
managed_files, _ = _make_managed_files_over_rows(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue